|
| 1 | +import os |
| 2 | +import shutil |
| 3 | +import requests |
| 4 | +import openpyxl |
| 5 | +from openpyxl.worksheet.datavalidation import DataValidation |
| 6 | +from openpyxl.styles import Alignment, Font |
| 7 | + |
| 8 | + |
| 9 | +class ChecklistProcessor: |
| 10 | + """Class to process and download checklist data from GitHub markdown files.""" |
| 11 | + |
| 12 | + def __init__(self, repo_document_root_url, tasvs_files): |
| 13 | + self.repo_document_root_url = repo_document_root_url |
| 14 | + self.tasvs_files = tasvs_files |
| 15 | + |
| 16 | + def get_testing_checklist(self, content): |
| 17 | + """Extract the 'Testing Checklist' section and its table from markdown content.""" |
| 18 | + lines = content.split("\n") |
| 19 | + start_idx, end_idx = -1, -1 |
| 20 | + checklist_lines = [] |
| 21 | + |
| 22 | + # Locate the "Testing Checklist" section |
| 23 | + for i, line in enumerate(lines): |
| 24 | + if "## Testing Checklist" in line: |
| 25 | + start_idx = i |
| 26 | + elif start_idx != -1 and line.startswith("#"): |
| 27 | + end_idx = i |
| 28 | + break |
| 29 | + |
| 30 | + # Extract the content if we found a section |
| 31 | + if start_idx != -1: |
| 32 | + checklist_lines = ( |
| 33 | + lines[start_idx + 1 : end_idx] |
| 34 | + if end_idx != -1 |
| 35 | + else lines[start_idx + 1 :] |
| 36 | + ) |
| 37 | + |
| 38 | + # Clean up the lines (remove empty lines or excess spaces) |
| 39 | + checklist_lines = [line.strip() for line in checklist_lines if line.strip()] |
| 40 | + |
| 41 | + # Parse the Markdown table from the checklist |
| 42 | + table_data = [] |
| 43 | + in_table = False |
| 44 | + for line in checklist_lines: |
| 45 | + if "----" not in line and "TASVS-ID" not in line: # Table row line |
| 46 | + row = [ |
| 47 | + cell.strip() for cell in line.split("|")[1:-1] |
| 48 | + ] # Split and remove leading/trailing pipes |
| 49 | + table_data.append(row) |
| 50 | + in_table = True |
| 51 | + elif in_table and not line.startswith("|"): |
| 52 | + break # Stop if we have exited the table section |
| 53 | + |
| 54 | + return table_data |
| 55 | + |
| 56 | + def process_files(self): |
| 57 | + """Download and process all markdown files, extracting checklist data.""" |
| 58 | + checklist_data = [] |
| 59 | + |
| 60 | + for file_name, sheet_name in self.tasvs_files: |
| 61 | + url = self.repo_document_root_url + file_name |
| 62 | + response = requests.get(url) |
| 63 | + |
| 64 | + if response.status_code == 200: |
| 65 | + content = response.text |
| 66 | + table_data = self.get_testing_checklist(content) |
| 67 | + if table_data: |
| 68 | + checklist_data.append((table_data, sheet_name)) |
| 69 | + print(f"Checklist table extracted from {file_name}") |
| 70 | + else: |
| 71 | + print( |
| 72 | + f"Failed to download {file_name} with status code {response.status_code}" |
| 73 | + ) |
| 74 | + |
| 75 | + return checklist_data |
| 76 | + |
| 77 | + |
| 78 | +class ExcelPopulator: |
| 79 | + """Class to handle the population of checklist data into an Excel template.""" |
| 80 | + |
| 81 | + def __init__(self, template_path, output_file_path, testing_notes_map): |
| 82 | + self.template_path = template_path |
| 83 | + self.output_file_path = output_file_path |
| 84 | + self.testing_notes_map = testing_notes_map |
| 85 | + self._prepare_workbook() |
| 86 | + |
| 87 | + def _prepare_workbook(self): |
| 88 | + """Create a copy of the original Excel file to work on.""" |
| 89 | + shutil.copy(self.template_path, self.output_file_path) |
| 90 | + |
| 91 | + def populate_spreadsheet(self, checklist_data, sheet_name): |
| 92 | + """Populate the extracted content into the corresponding Excel sheet.""" |
| 93 | + wb = openpyxl.load_workbook(self.output_file_path) |
| 94 | + |
| 95 | + if sheet_name not in wb.sheetnames: |
| 96 | + print(f"Sheet '{sheet_name}' not found in the workbook.") |
| 97 | + return |
| 98 | + |
| 99 | + sheet = wb[sheet_name] |
| 100 | + dropdown_options = ["Failed", "N/A", "Pending", "Reviewed"] |
| 101 | + dropdown = DataValidation( |
| 102 | + type="list", formula1=f'"{",".join(dropdown_options)}"', allow_blank=True |
| 103 | + ) |
| 104 | + dropdown.error = "Invalid entry, please select from the dropdown options." |
| 105 | + dropdown.prompt = "Please select an option from the list." |
| 106 | + sheet.add_data_validation(dropdown) |
| 107 | + |
| 108 | + notes_dict = {item[0]: item[1] for item in self.testing_notes_map} |
| 109 | + start_row = 12 |
| 110 | + |
| 111 | + for row_data in checklist_data: |
| 112 | + sheet[f"B{start_row}"] = row_data[0] if row_data[0] else "" |
| 113 | + sheet[f"C{start_row}"] = row_data[1] if row_data[1] else "" |
| 114 | + sheet[f"D{start_row}"] = row_data[2] if row_data[2] else "" |
| 115 | + sheet[f"E{start_row}"] = row_data[3] if row_data[3] else "" |
| 116 | + sheet[f"F{start_row}"] = row_data[4] if row_data[4] else "" |
| 117 | + |
| 118 | + tasvs_id = row_data[0] |
| 119 | + if tasvs_id in notes_dict: |
| 120 | + sheet[f"K{start_row}"] = notes_dict[tasvs_id] |
| 121 | + |
| 122 | + non_empty_cells = sum(1 for item in row_data if item) |
| 123 | + |
| 124 | + if non_empty_cells == 2: |
| 125 | + for col in range(2, 3): # Columns B (2) to C (3) |
| 126 | + sheet.cell(row=start_row, column=col).font = Font(bold=True) |
| 127 | + sheet.row_dimensions[start_row].height = 26 |
| 128 | + else: |
| 129 | + dropdown.add(sheet[f"G{start_row}"]) |
| 130 | + sheet.row_dimensions[start_row].height = 90 |
| 131 | + |
| 132 | + start_row += 1 |
| 133 | + |
| 134 | + self._apply_global_formatting(sheet, start_row) |
| 135 | + wb.save(self.output_file_path) |
| 136 | + print( |
| 137 | + f"Data populated successfully into sheet '{sheet_name}' in {self.output_file_path}" |
| 138 | + ) |
| 139 | + |
| 140 | + def _apply_global_formatting(self, sheet, end_row): |
| 141 | + """Apply global formatting across the sheet.""" |
| 142 | + for row in range(12, end_row + 1): |
| 143 | + for col in range(2, 12): |
| 144 | + cell = sheet.cell(row=row, column=col) |
| 145 | + cell.alignment = Alignment( |
| 146 | + horizontal="left", vertical="center", wrap_text=True |
| 147 | + ) |
| 148 | + |
| 149 | + |
| 150 | +class TASVSConversion: |
| 151 | + """Main class to manage the TASVS conversion process.""" |
| 152 | + |
| 153 | + def __init__( |
| 154 | + self, |
| 155 | + repo_document_root_url, |
| 156 | + tasvs_files, |
| 157 | + template_path, |
| 158 | + output_file_path, |
| 159 | + testing_notes_map, |
| 160 | + ): |
| 161 | + self.checklist_processor = ChecklistProcessor( |
| 162 | + repo_document_root_url, tasvs_files |
| 163 | + ) |
| 164 | + self.excel_populator = ExcelPopulator( |
| 165 | + template_path, output_file_path, testing_notes_map |
| 166 | + ) |
| 167 | + |
| 168 | + def run(self): |
| 169 | + """Run the complete TASVS conversion process.""" |
| 170 | + checklist_data = self.checklist_processor.process_files() |
| 171 | + |
| 172 | + for table_data, sheet_name in checklist_data: |
| 173 | + self.excel_populator.populate_spreadsheet(table_data, sheet_name) |
| 174 | + |
| 175 | + |
| 176 | +if __name__ == "__main__": |
| 177 | + repo_document_root_url = "https://raw.githubusercontent.com/OWASP/www-project-thick-client-application-security-verification-standard/main/document/1.0/" |
| 178 | + tasvs_files = [ |
| 179 | + ("04-TASVS-ARCH.md", "TASVS-ARCH"), |
| 180 | + ("05-TASVS-CODE.md", "TASVS-CODE"), |
| 181 | + ("06-TASVS-CONF.md", "TASVS-CONF"), |
| 182 | + ("07-TASVS-CRYPTO.md", "TASVS-CRYPTO"), |
| 183 | + ("08-TASVS-NETWORK.md", "TASVS-NETWORK"), |
| 184 | + ("09-TASVS-STORAGE.md", "TASVS-STORAGE"), |
| 185 | + ] |
| 186 | + template_path = os.path.join( |
| 187 | + os.path.dirname(os.path.realpath(__file__)), "TASVS_V0.99999999_orig.xlsx" |
| 188 | + ) |
| 189 | + |
| 190 | + response = requests.get( |
| 191 | + "https://api.github.com/repos/OWASP/www-project-thick-client-application-security-verification-standard/releases/latest" |
| 192 | + ) |
| 193 | + |
| 194 | + if response.status_code == 200: |
| 195 | + # Parse the JSON response |
| 196 | + latest_release = response.json() |
| 197 | + latest_tag = latest_release["tag_name"] |
| 198 | + else: |
| 199 | + print(f"Failed to retrieve latest release. Status code: {response.status_code}") |
| 200 | + # default the tag to something so it looks good in the filename |
| 201 | + latest_tag = "v1.0" |
| 202 | + |
| 203 | + output_file_path = os.path.join( |
| 204 | + os.path.dirname(os.path.realpath(__file__)), f"TASVS_{latest_tag}.xlsx" |
| 205 | + ) |
| 206 | + |
| 207 | + # map for data to be inserted into col K "Testing notes". Format: |
| 208 | + # (TASVS-ID, Note, [hyperlink]) |
| 209 | + # |
| 210 | + # fmt is to tell black to ignore the formatting |
| 211 | + # fmt: off |
| 212 | + testing_notes_map = [ |
| 213 | + ("TASVS-ARCH-1.1", "TASVS-ARCH-1.1 satisfied by TASVS-ARCH-1.3", ""), |
| 214 | + ("TASVS-ARCH-1.2", "Fidelity score >= 80%", ""), |
| 215 | + ("TASVS-ARCH-1.3", "External dependencies can be defined as out-of-scope where appropriate.", ""), |
| 216 | + ("TASVS-ARCH-1.6", "Within prior 90 days", ""), |
| 217 | + ("TASVS-CODE-1.1", "Recommended: OWASP ASVS", "https://owasp.org/www-project-application-security-verification-standard/"), |
| 218 | + ("TASVS-CODE-2.5", "Example: C Hardening Cheat Sheet", "https://cheatsheetseries.owasp.org/cheatsheets/C-Based_Toolchain_Hardening_Cheat_Sheet.html"), |
| 219 | + ("TASVS-CODE-3.1", "Recommended: OWASP Dependency-Check", "https://github.com/jeremylong/DependencyCheck"), |
| 220 | + ("TASVS-CODE-3.3", "Recommended: BinSkim", "https://github.com/microsoft/binskim"), |
| 221 | + ("TASVS-CODE-3.4", "E.g: Python=Bandit, C#=Security Code Scan. Fallback to OWASP cheatsheets and/or use SemGrep.", ""), |
| 222 | + ("TASVS-CODE-3.6", "Case Study", "https://www.henricodolfing.com/2019/06/project-failure-case-study-knight-capital.html"), |
| 223 | + ("TASVS-CODE-4.10", "Example: batbadbut research", "https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/"), |
| 224 | + ("TASVS-CODE-6.2", "Also satisfies TASVS-CODE-6.1", ""), |
| 225 | + ] |
| 226 | + # fmt: on |
| 227 | + |
| 228 | + conversion = TASVSConversion( |
| 229 | + repo_document_root_url, |
| 230 | + tasvs_files, |
| 231 | + template_path, |
| 232 | + output_file_path, |
| 233 | + testing_notes_map, |
| 234 | + ) |
| 235 | + conversion.run() |
0 commit comments