├── LICENSE ├── README.md ├── index.html ├── requirements.txt ├── sample.html ├── sample_enriched.html └── sunshine.py /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ▗▄▄▖▗▖ ▗▖▗▖ ▗▖ ▗▄▄▖▗▖ ▗▖▗▄▄▄▖▗▖ ▗▖▗▄▄▄▖ 3 | ▐▌ ▐▌ ▐▌▐▛▚▖▐▌▐▌ ▐▌ ▐▌ █ ▐▛▚▖▐▌▐▌ 4 | ▝▀▚▖▐▌ ▐▌▐▌ ▝▜▌ ▝▀▚▖▐▛▀▜▌ █ ▐▌ ▝▜▌▐▛▀▀▘ 5 | ▗▄▄▞▘▝▚▄▞▘▐▌ ▐▌▗▄▄▞▘▐▌ ▐▌▗▄█▄▖▐▌ ▐▌▐▙▄▄▖ 6 | ``` 7 | 8 | Sunshine: actionable CycloneDX visualization tool. 9 |

10 | It takes a JSON CycloneDX file as input and provides as output an HTML containing a chart and table representation of the components, dependencies, vulnerabilities and licenses. It can also enrich data by adding EPSS and CISA KEV information. See a sample HTML output [here without enriched data](https://cyclonedx.github.io/Sunshine/sample.html) and [here with enriched data](https://cyclonedx.github.io/Sunshine/sample_enriched.html). 11 | 12 |
13 | 14 | Can be used in 2 ways: 15 | - As a web application: all submitted data is processed locally within your browser, without being transmitted anywhere else. 16 | - As a standalone CLI tool. 17 |
18 | 19 | Usage of the web application: 20 | - option 1: via the online version at URL https://cyclonedx.github.io/Sunshine/ 21 | - option 2: by running `python3 -m http.server 8000` and opening a browser at URL http://127.0.0.1:8000 22 | 23 |
24 | Usage of the CLI version: 25 | 26 | ``` 27 | pip3 install -r requirements.txt 28 | 29 | sunshine.py [-h] [-v] [-i INPUT] [-o OUTPUT] 30 | 31 | options: 32 | -h, --help show this help message and exit 33 | -v, --version show program version 34 | -i, --input INPUT path of input CycloneDX file 35 | -o, --output OUTPUT path of output HTML file 36 | -e, --enrich enrich CVEs with EPSS and CISA KEV 37 | ``` 38 | 39 |
40 | 41 | Credits: 42 | - made by: [Luca Capacci](https://www.linkedin.com/in/lucacapacci/) 43 | - contributor: [Mattia Fierro](https://www.linkedin.com/in/mattiafierro/) 44 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sunshine - SBOM visualization tool 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 169 | 170 | 171 | 172 |

Sunshine - SBOM visualization tool

173 |
174 |

CycloneDX JSON file

175 |
Select a CycloneDX JSON file from your file system. All submitted data is processed locally within your browser, without being transmitted anywhere else. 176 | 181 |
182 | 183 | 184 |
185 |
186 | 187 |
188 |

Summary

189 | 192 |
193 | Summary table will appear here... 194 |
195 | 196 |
197 |

Components chart

198 | 238 |
239 | Chart will appear here... 240 |
241 |
242 |

Components table

243 | 269 |
270 | Components table will appear here... 271 |
272 | 273 |
274 |

Vulnerabilities table

275 | 299 |
300 | Vulnerabilities table will appear here... 301 |
302 | 303 |
304 |

Log

305 |
Log will appear here...
306 |

307 | 308 | 309 | 695 | 696 | 697 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /sunshine.py: -------------------------------------------------------------------------------- 1 | # This file is part of CycloneDX Sunshine 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # Copyright (c) OWASP Foundation. All Rights Reserved. 17 | 18 | 19 | __all__ = [ 20 | # This module does not export any symbols; all sumbols are private/internal. 21 | ] 22 | 23 | import json 24 | import argparse 25 | import os 26 | import html 27 | import copy 28 | import re 29 | import requests 30 | import csv 31 | from decimal import Decimal 32 | 33 | 34 | VERSION = "0.9" 35 | NAME = "Sunshine" 36 | 37 | PREFERRED_VULNERABILITY_RATING_METHODS_ORDER = ["CVSSv4", 38 | "CVSSv31", 39 | "CVSSv3", 40 | "CVSSv2", 41 | "OWASP", 42 | "SSVC", 43 | "other"] 44 | 45 | VALID_SEVERITIES = {"critical": 4, 46 | "high": 3, 47 | "medium": 2, 48 | "low": 1, 49 | "info": 0, 50 | "information": 0, 51 | "clean": -1} 52 | 53 | GREY = '#bcbcbc' 54 | GREEN = '#7dd491' 55 | YELLOW = '#fccd58' 56 | ORANGE = '#ff9335' 57 | RED = '#ff4633' 58 | DARK_RED = '#a10a0a' 59 | LIGHT_BLUE = '#9fc5e8' 60 | 61 | BASIC_STYLE = { "color": GREY, "borderWidth": 2 } 62 | INFORMATION_STYLE = { "color": GREEN, "borderWidth": 2 } 63 | LOW_STYLE = { "color": YELLOW, "borderWidth": 2 } 64 | MEDIUM_STYLE = { "color": ORANGE, "borderWidth": 2 } 65 | HIGH_STYLE = { "color": RED, "borderWidth": 2 } 66 | CRITICAL_STYLE = { "color": DARK_RED, "borderWidth": 2 } 67 | TRANSITIVE_VULN_STYLE = { "color": LIGHT_BLUE, "borderWidth": 2 } 68 | 69 | 70 | STYLES = {"critical": CRITICAL_STYLE, 71 | "high": HIGH_STYLE, 72 | "medium": MEDIUM_STYLE, 73 | "low": LOW_STYLE, 74 | "information": INFORMATION_STYLE, 75 | "clean": BASIC_STYLE} 76 | 77 | 78 | HTML_TEMPLATE = """ 79 | 80 | 81 | 82 | 83 | 84 | Sunshine - SBOM visualization tool 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 244 | 245 | 246 | 247 | 248 |

Sunshine - SBOM visualization tool

249 |
250 |
251 | Analyzed CycloneDX JSON file: 252 |
253 | 254 |
255 |

Summary

256 |
257 |
258 |
259 | 260 |

261 |

Components chart

262 |
263 | This chart visualizes components and their dependencies, with each segment representing a single component. The chart provides a hierarchical view of the dependency structure, with relationships radiating outward from the core components.
264 | 268 | Note: If there is only one circle, it means that no dependency relationships are defined in the input file. 269 |

270 | The colors of the segments indicate the vulnerability status of the components: 271 | 280 | The chart is interactive: 281 | 285 |
286 |
287 | 288 | 291 |
292 |
293 | 294 | 297 |
298 |
299 |
300 | 301 |
302 |
303 |

Components table

304 |
305 | This table visualizes components, their dependencies, vulnerabilities and licenses.
306 | The colors of the elements in columns "Component", "Depends on" and "Dependency of" indicate the vulnerability status of the components: 307 | 316 |
317 | The colors of the elements in columns "Direct vulnerabilities" and "Transitive vulnerabilities" indicate the severity of the vulnerabilities: 318 | 325 |

326 |
327 |
328 |
329 |
330 |
331 |

Vulnerabilities table

332 |
333 | This table focuses on vulnerabilities and shows the components that are affected either directly or transitively.
334 | The colors of the elements in column "Vulnerability" indicate the severity of the vulnerabilities: 335 | 342 |
343 | The colors of the elements in columns "Directly vulnerable components" and "Transitively vulnerable components" indicate the vulnerability status of the components: 344 | 352 |

353 |
354 |
355 | 586 |

587 | 588 | 589 | 590 | """ 591 | 592 | 593 | def custom_print(text): 594 | if __name__ == "__web__": 595 | from js import writeToLog 596 | writeToLog(text) 597 | else: 598 | print(text) 599 | 600 | 601 | class SetEncoder(json.JSONEncoder): 602 | def default(self, obj): 603 | if isinstance(obj, set): 604 | return list(obj) 605 | return super().default(obj) 606 | 607 | 608 | def create_fake_component(bom_ref): 609 | return {"name": bom_ref, 610 | "version": "-", 611 | "type": "-", 612 | "license": set(), 613 | "depends_on": set(), 614 | "dependency_of": set(), 615 | "vulnerabilities": [], 616 | "transitive_vulnerabilities": [], 617 | "max_vulnerability_severity": "clean", 618 | "has_transitive_vulnerabilities": False, 619 | "visited": False} 620 | 621 | 622 | def create_base_component(component): 623 | new_component = {"name": component["name"], 624 | "version": component["version"] if "version" in component else "-", 625 | "type": component["type"] if "type" in component else "-", 626 | "license": parse_licenses(component), 627 | "depends_on": set(), 628 | "dependency_of": set(), 629 | "vulnerabilities": [], 630 | "transitive_vulnerabilities": [], 631 | "max_vulnerability_severity": "clean", 632 | "has_transitive_vulnerabilities": False, 633 | "visited": False} 634 | 635 | return new_component 636 | 637 | def get_severity_by_score(score): 638 | score = float(score) 639 | if score >= 9: 640 | return "critical" 641 | elif score >= 7: 642 | return "high" 643 | elif score >= 4: 644 | return "medium" 645 | elif score > 0: 646 | return "low" 647 | else: 648 | return "information" 649 | 650 | 651 | def get_preferred_vuln_source(source_1, source_2): 652 | source_1 = source_1.upper() 653 | source_2 = source_2.upper() 654 | 655 | if source_1 == "NVD": 656 | source_1_order = 0 657 | elif source_1 in ["-", "EPSS"]: 658 | source_1_order = 2 659 | else: 660 | source_1_order = 1 661 | 662 | if source_2 == "NVD": 663 | source_2_order = 0 664 | elif source_2 in ["-", "EPSS"]: 665 | source_2_order = 2 666 | else: 667 | source_2_order = 1 668 | 669 | if source_1_order < source_2_order: 670 | return source_1 671 | else: 672 | return source_2 673 | 674 | 675 | def parse_vulnerability_data(vulnerability): 676 | vuln_id = vulnerability["id"] 677 | 678 | vuln_severity = None 679 | vuln_score = 0.0 680 | vuln_vector = "-" 681 | vuln_source = "-" 682 | found_at_least_one = False 683 | if "ratings" in vulnerability: 684 | for preferred_rating_method in PREFERRED_VULNERABILITY_RATING_METHODS_ORDER: 685 | if found_at_least_one is True: 686 | break 687 | for rating in vulnerability["ratings"]: 688 | if "method" not in rating: 689 | continue 690 | if rating["method"] == preferred_rating_method: 691 | found_at_least_one = True 692 | current_vuln_score = 0.0 693 | current_vuln_vector = "-" 694 | current_vuln_source = "-" 695 | if "severity" in rating and rating["severity"].lower() in VALID_SEVERITIES: 696 | current_vuln_severity = rating["severity"] 697 | if current_vuln_severity.lower() == "info": 698 | current_vuln_severity = "information" 699 | current_vuln_severity = current_vuln_severity.lower() 700 | if "score" in rating: 701 | current_vuln_score = float(rating["score"]) 702 | if "vector" in rating: 703 | current_vuln_vector = rating["vector"] 704 | if "source" in rating: 705 | if "name" in rating["source"]: 706 | current_vuln_source = rating["source"]["name"] 707 | elif "score" in rating: 708 | current_vuln_severity = get_severity_by_score(rating["score"]) 709 | current_vuln_score = float(rating["score"]) 710 | if "vector" in rating: 711 | current_vuln_vector = rating["vector"] 712 | if "source" in rating: 713 | if "name" in rating["source"]: 714 | current_vuln_source = rating["source"]["name"] 715 | 716 | if get_preferred_vuln_source(vuln_source, current_vuln_source) == current_vuln_source: 717 | vuln_severity = current_vuln_severity 718 | vuln_score = current_vuln_score 719 | vuln_vector = current_vuln_vector 720 | vuln_source = current_vuln_source 721 | 722 | if vuln_severity is None: 723 | if "ratings" not in vulnerability: 724 | custom_print(f"WARNING: vulnerability with id '{vulnerability['id']}' does not have a 'ratings' field. I'll set a default 'INFORMATION' severity...") 725 | vuln_severity = get_severity_by_score(0) 726 | elif len(vulnerability["ratings"]) == 0: 727 | custom_print(f"WARNING: vulnerability with id '{vulnerability['id']}' does have an empty 'ratings' field. I'll set a default 'INFORMATION' severity...") 728 | vuln_severity = get_severity_by_score(0) 729 | else: 730 | for rating in vulnerability["ratings"]: 731 | if "severity" in rating: 732 | rating_vuln_severity = rating["severity"] 733 | if rating_vuln_severity.lower() in VALID_SEVERITIES: 734 | vuln_severity = rating_vuln_severity.lower() 735 | if "score" in rating: 736 | vuln_score = float(rating["score"]) 737 | if "vector" in rating: 738 | vuln_vector = rating["vector"] 739 | break 740 | if "score" in rating: 741 | vuln_severity = get_severity_by_score(rating["score"]) 742 | vuln_score = float(rating["score"]) 743 | if "vector" in rating: 744 | vuln_vector = rating["vector"] 745 | break 746 | 747 | if vuln_severity is None: 748 | custom_print(f"WARNING: could not detect severity of vulnerability with id '{vulnerability['id']}'. I'll set a default 'INFORMATION' severity...") 749 | vuln_severity = get_severity_by_score(0) 750 | 751 | print(f"{vuln_source=}") 752 | return vuln_id, vuln_severity, vuln_score, vuln_vector 753 | 754 | 755 | bom_ref_cache = {} 756 | def get_bom_ref(component_json, all_bom_refs): 757 | global bom_ref_cache 758 | if "bom-ref" in component_json: 759 | bom_ref = component_json["bom-ref"] 760 | return bom_ref 761 | else: 762 | bom_ref_cache_key = f"{component_json['name']} - {component_json['version']}" 763 | if bom_ref_cache_key in bom_ref_cache: 764 | return bom_ref_cache[f"{component_json['name']} - {component_json['version']}"] 765 | 766 | custom_print(f"WARNING: component with name '{component_json['name']}' and version '{component_json['version']}' does not have a 'bom-ref'. I'll search for a match...") 767 | for potential_bom_ref in all_bom_refs: 768 | guessed_name_01 = f'{component_json["name"]}@{component_json["version"]}' 769 | guessed_name_02 = f'{component_json["name"]}::{component_json["version"]}' 770 | guessed_name_03 = f'{component_json["name"]}:{component_json["version"]}' 771 | 772 | for test in [guessed_name_01, guessed_name_02, guessed_name_03]: 773 | if potential_bom_ref.endswith(f"/{test}"): 774 | custom_print(f"Match found: {potential_bom_ref}") 775 | bom_ref_cache[bom_ref_cache_key] = potential_bom_ref 776 | return potential_bom_ref 777 | if potential_bom_ref.endswith(f"/{test}:"): 778 | custom_print(f"Match found: {potential_bom_ref}") 779 | bom_ref_cache[bom_ref_cache_key] = potential_bom_ref 780 | return potential_bom_ref 781 | if potential_bom_ref.endswith(f":{test}"): 782 | bom_ref_cache[bom_ref_cache_key] = potential_bom_ref 783 | custom_print(f"Match found: {potential_bom_ref}") 784 | return potential_bom_ref 785 | if potential_bom_ref.endswith(f":{test}:"): 786 | bom_ref_cache[bom_ref_cache_key] = potential_bom_ref 787 | custom_print(f"Match found: {potential_bom_ref}") 788 | return potential_bom_ref 789 | 790 | # another try with version not in the end of the string 791 | number_of_results = 0 792 | result = None 793 | for potential_bom_ref in all_bom_refs: 794 | guessed_name_01 = f'{component_json["name"]}@{component_json["version"]}' 795 | guessed_name_02 = f'{component_json["name"]}::{component_json["version"]}' 796 | guessed_name_03 = f'{component_json["name"]}:{component_json["version"]}' 797 | 798 | for test in [guessed_name_01, guessed_name_02, guessed_name_03]: 799 | if f"/{test}:" in bom_ref: 800 | number_of_results += 1 801 | result = potential_bom_ref 802 | elif f":{test}:" in bom_ref: 803 | number_of_results += 1 804 | result = potential_bom_ref 805 | if number_of_results == 1: # I want just one result, otherwise it means the sbom is ambiguous and I can't make any educated guess 806 | bom_ref_cache[bom_ref_cache_key] = result 807 | return result 808 | 809 | custom_print(f"Match not found. I'll create a fake one.") 810 | bom_ref = f"{hash(json.dumps(component_json, sort_keys=True, cls=SetEncoder))}" 811 | bom_ref_cache[bom_ref_cache_key] = bom_ref 812 | return bom_ref 813 | 814 | 815 | def create_or_update_bom_ref_entry(bom_refs, component): 816 | if component["bom-ref"] not in bom_refs: 817 | bom_refs[component["bom-ref"]] = {"name": component["name"] if "name" in component else "-", 818 | "version": component["version"] if "version" in component else "-"} 819 | else: 820 | if bom_refs[component["bom-ref"]]["name"] == "-" and "name" in component: 821 | bom_refs[component["bom-ref"]]["name"] = component["name"] 822 | if bom_refs[component["bom-ref"]]["version"] == "-" and "version" in component: 823 | bom_refs[component["bom-ref"]]["version"] = component["version"] 824 | 825 | 826 | def normalize_bom_ref(bom_refs, bom_ref, only_valid_components=True): 827 | for component_bom_ref, component_data in bom_refs.items(): 828 | if only_valid_components is False: 829 | if bom_ref == component_bom_ref: 830 | return bom_ref 831 | else: 832 | if bom_ref == component_bom_ref and component_data["name"] != "-" and component_data["version"] != "-": 833 | return bom_ref 834 | 835 | for component_bom_ref, component_data in bom_refs.items(): 836 | # look with version 837 | guessed_name_01 = f'{component_data["name"]}@{component_data["version"]}' 838 | guessed_name_02 = f'{component_data["name"]}::{component_data["version"]}' 839 | guessed_name_03 = f'{component_data["name"]}:{component_data["version"]}' 840 | 841 | for test in [guessed_name_01, guessed_name_02, guessed_name_03]: 842 | if bom_ref.endswith(f"/{test}"): 843 | return bom_ref 844 | if bom_ref.endswith(f"/{test}:"): 845 | return bom_ref 846 | if bom_ref.endswith(f":{test}"): 847 | return bom_ref 848 | if bom_ref.endswith(f":{test}:"): 849 | return bom_ref 850 | 851 | # another try with version not in the end of the string 852 | number_of_results = 0 853 | result = None 854 | for component_bom_ref, component_data in bom_refs.items(): 855 | guessed_name_01 = f'{component_data["name"]}@{component_data["version"]}' 856 | guessed_name_02 = f'{component_data["name"]}::{component_data["version"]}' 857 | guessed_name_03 = f'{component_data["name"]}:{component_data["version"]}' 858 | 859 | for test in [guessed_name_01, guessed_name_02, guessed_name_03]: 860 | if f"/{test}:" in bom_ref: 861 | number_of_results += 1 862 | result = component_bom_ref 863 | elif f":{test}:" in bom_ref: 864 | number_of_results += 1 865 | result = component_bom_ref 866 | if number_of_results == 1: # I want just one result, otherwise it means the sbom is ambiguous and I can't make any educated guess 867 | return result 868 | 869 | # final try: without version 870 | number_of_results = 0 871 | result = None 872 | for component_bom_ref, component_data in bom_refs.items(): 873 | # look without version 874 | if f'/{component_data["name"]}@' in bom_ref: 875 | number_of_results += 1 876 | result = component_bom_ref 877 | elif f'/{component_data["name"]}:' in bom_ref: 878 | number_of_results += 1 879 | result = component_bom_ref 880 | elif f':{component_data["name"]}:' in bom_ref: 881 | number_of_results += 1 882 | result = component_bom_ref 883 | elif f':{component_data["name"]}@' in bom_ref: 884 | number_of_results += 1 885 | result = component_bom_ref 886 | if number_of_results == 1: # I want just one result, otherwise it means the sbom is ambiguous and I can't make any educated guess 887 | return result 888 | 889 | return None 890 | 891 | 892 | def has_bom_ref_components(bom_refs, bom_ref): 893 | return normalize_bom_ref(bom_refs, bom_ref, only_valid_components=False) is not None 894 | 895 | 896 | def add_nested_components(component, components, all_bom_refs): 897 | for child_key in ["services", "components"]: 898 | if child_key in component: 899 | for sub_component in component[child_key]: 900 | new_component = create_base_component(sub_component) 901 | bom_ref = get_bom_ref(sub_component, all_bom_refs) 902 | components[bom_ref] = new_component 903 | add_nested_components(sub_component, components, all_bom_refs) 904 | 905 | 906 | def detect_nested_bom_refs(component, bom_refs): 907 | for child_key in ["services", "components"]: 908 | if child_key in component: 909 | for sub_component in component[child_key]: 910 | if "bom-ref" in sub_component: 911 | create_or_update_bom_ref_entry(bom_refs, sub_component) 912 | 913 | 914 | def get_all_bom_refs(data): 915 | bom_refs = {} 916 | meta_bom_ref_is_used = False 917 | 918 | root_keywords = [] 919 | if "components" in data: 920 | root_keywords.append("components") 921 | if "services" in data: 922 | root_keywords.append("services") 923 | 924 | for root_keyword in root_keywords: 925 | for component in data[root_keyword]: 926 | if "bom-ref" in component: 927 | create_or_update_bom_ref_entry(bom_refs, component) 928 | 929 | detect_nested_bom_refs(component, bom_refs) 930 | 931 | for root_keyword in root_keywords: 932 | for component in data[root_keyword]: 933 | if "dependencies" in component: 934 | for dependency in component["dependencies"]: 935 | if "ref" in dependency: 936 | create_or_update_bom_ref_entry(bom_refs, {"bom-ref": dependency["ref"]}) 937 | 938 | if "dependencies" in data: 939 | for dependency in data["dependencies"]: 940 | if "ref" in dependency: 941 | create_or_update_bom_ref_entry(bom_refs, {"bom-ref": dependency["ref"]}) 942 | 943 | if "dependsOn" in dependency: 944 | for depends_on in dependency["dependsOn"]: 945 | create_or_update_bom_ref_entry(bom_refs, {"bom-ref": depends_on}) 946 | 947 | if "metadata" in data: 948 | if "component" in data["metadata"]: 949 | if "bom-ref" in data["metadata"]["component"]: 950 | if has_bom_ref_components(bom_refs, data["metadata"]["component"]["bom-ref"]): 951 | meta_bom_ref_is_used = True 952 | create_or_update_bom_ref_entry(bom_refs, data["metadata"]["component"]) 953 | 954 | return bom_refs, meta_bom_ref_is_used 955 | 956 | 957 | def parse_licenses(component): 958 | licenses = set() 959 | if "licenses" in component: 960 | for license in component["licenses"]: 961 | if "license" in license: 962 | if "id" in license["license"]: 963 | licenses.add(license["license"]["id"]) 964 | elif "name" in license["license"]: 965 | licenses.add(license["license"]["name"]) 966 | return sorted(list(licenses)) 967 | 968 | 969 | def parse_metadata(data): 970 | metadata_info = {} 971 | 972 | metadata_field = None 973 | 974 | if "metadata" in data: 975 | metadata_field = data["metadata"] 976 | 977 | if metadata_field is not None: 978 | if "component" in metadata_field: 979 | metadata_info["Main Component"] = {} 980 | if "type" in metadata_field["component"]: 981 | metadata_info["Main Component"]["Type"] = metadata_field["component"]["type"] 982 | if "group" in metadata_field["component"]: 983 | metadata_info["Main Component"]["Group"] = metadata_field["component"]["group"] 984 | if "name" in metadata_field["component"]: 985 | metadata_info["Main Component"]["Name"] = metadata_field["component"]["name"] 986 | if "version" in metadata_field["component"]: 987 | metadata_info["Main Component"]["Version"] = metadata_field["component"]["version"] 988 | if "description" in metadata_field["component"]: 989 | metadata_info["Main Component"]["Description"] = metadata_field["component"]["description"] 990 | if "purl" in metadata_field["component"]: 991 | metadata_info["Main Component"]["PURL"] = metadata_field["component"]["purl"] 992 | 993 | if "properties" in metadata_field["component"]: 994 | for property_element in metadata_field["component"]["properties"]: 995 | metadata_info["Main Component"][f'{property_element["name"][0].capitalize()}{property_element["name"][1:]}'] = property_element["value"] 996 | 997 | if "specVersion" in data: 998 | metadata_info["Spec Version"] = data["specVersion"] 999 | 1000 | if "serialNumber" in data: 1001 | metadata_info["Serial Number"] = data["serialNumber"] 1002 | 1003 | if "version" in data: 1004 | metadata_info["Version"] = str(data["version"]) 1005 | 1006 | if metadata_field is not None: 1007 | if "tools" in metadata_field: 1008 | counter = 0 1009 | for tool in metadata_field["tools"]: 1010 | counter += 1 1011 | info_id = "Tool" 1012 | if len(metadata_field["tools"]) > 1: 1013 | info_id = f"{info_id} #{counter}" 1014 | 1015 | metadata_info[info_id] = {} 1016 | if "vendor" in tool: 1017 | metadata_info[info_id]["Vendor"] = tool["vendor"] 1018 | if "name" in tool: 1019 | metadata_info[info_id]["Name"] = tool["name"] 1020 | if "version" in tool: 1021 | metadata_info[info_id]["Version"] = tool["version"] 1022 | if "services" in tool: 1023 | counter_services = 0 1024 | services = metadata_field["tools"]["services"] 1025 | 1026 | for service in services: 1027 | counter_services += 1 1028 | field_id = "Service" 1029 | if len(services) > 1: 1030 | field_id = f"Service #{counter_services}" 1031 | 1032 | if "vendor" in service: 1033 | metadata_info[info_id][f"{field_id} Vendor"] = service["vendor"] 1034 | if "name" in service: 1035 | metadata_info[info_id][f"{field_id} Name"] = service["name"] 1036 | if "version" in service: 1037 | metadata_info[info_id][f"{field_id} Version"] = service["version"] 1038 | 1039 | return metadata_info 1040 | 1041 | 1042 | def parse_json_data(data): 1043 | all_bom_refs, meta_bom_ref_is_used = get_all_bom_refs(data) 1044 | 1045 | guessed_bom_refs_cache = {} 1046 | 1047 | components = {} 1048 | 1049 | root_keywords = [] 1050 | if "components" in data: 1051 | root_keywords.append("components") 1052 | if "services" in data: 1053 | root_keywords.append("services") 1054 | 1055 | if "metadata" in data: 1056 | if "component" in data["metadata"]: 1057 | component = data["metadata"]["component"] 1058 | if meta_bom_ref_is_used is True: 1059 | new_component = create_base_component(component) 1060 | bom_ref = get_bom_ref(component, all_bom_refs) 1061 | components[bom_ref] = new_component 1062 | 1063 | metadata_info = parse_metadata(data) 1064 | 1065 | for root_keyword in root_keywords: 1066 | for component in data[root_keyword]: 1067 | new_component = create_base_component(component) 1068 | bom_ref = get_bom_ref(component, all_bom_refs) 1069 | components[bom_ref] = new_component 1070 | add_nested_components(component, components, all_bom_refs) 1071 | 1072 | # sometimes dependencies are declared inside a component, I'll check that now 1073 | for component in data[root_keyword]: 1074 | bom_ref = get_bom_ref(component, all_bom_refs) 1075 | 1076 | if "dependencies" in component: 1077 | for dependency in component["dependencies"]: 1078 | depends_on = dependency["ref"] 1079 | if depends_on not in components: 1080 | if depends_on in guessed_bom_refs_cache: 1081 | depends_on = guessed_bom_refs_cache[depends_on] 1082 | else: 1083 | custom_print(f"WARNING: 'ref' '{depends_on}' is used in 'dependencies' inside a component but it's not declared in 'components'. I'll search for a match...") 1084 | guessed_bom_ref = normalize_bom_ref(all_bom_refs, depends_on) 1085 | guessed_bom_refs_cache[depends_on] = guessed_bom_ref 1086 | 1087 | if guessed_bom_ref is None: 1088 | custom_print(f"Match not found. I'll create a fake one.") 1089 | components[depends_on] = create_fake_component(depends_on) 1090 | else: 1091 | custom_print(f"Match found: {guessed_bom_ref}") 1092 | depends_on = guessed_bom_ref 1093 | 1094 | components[bom_ref]["depends_on"].add(depends_on) 1095 | components[depends_on]["dependency_of"].add(bom_ref) 1096 | 1097 | # sometimes vulnerabilities are declared inside a component, I'll check that now 1098 | for component in data[root_keyword]: 1099 | bom_ref = get_bom_ref(component, all_bom_refs) 1100 | 1101 | if "vulnerabilities" in component: 1102 | for vulnerability in component["vulnerabilities"]: 1103 | vuln_id, vuln_severity, vuln_score, vuln_vector = parse_vulnerability_data(vulnerability) 1104 | 1105 | vulnerability_data = {"id": vuln_id, "severity": vuln_severity, "score": vuln_score, "vector": vuln_vector} 1106 | if vulnerability_data not in components[bom_ref]["vulnerabilities"]: 1107 | components[bom_ref]["vulnerabilities"].append(vulnerability_data) 1108 | if VALID_SEVERITIES[vuln_severity] > VALID_SEVERITIES[components[bom_ref]["max_vulnerability_severity"]]: 1109 | components[bom_ref]["max_vulnerability_severity"] = vuln_severity 1110 | 1111 | if "dependencies" in data: 1112 | for dependency in data["dependencies"]: 1113 | bom_ref = dependency["ref"] 1114 | if bom_ref not in components: 1115 | if bom_ref in guessed_bom_refs_cache: 1116 | bom_ref = guessed_bom_refs_cache[bom_ref] 1117 | else: 1118 | custom_print(f"WARNING: 'ref' '{bom_ref}' is used in 'dependencies' in a 'ref' field but it's not declared in 'components'. I'll search for a match...") 1119 | guessed_bom_ref = normalize_bom_ref(all_bom_refs, bom_ref) 1120 | guessed_bom_refs_cache[bom_ref] = guessed_bom_ref 1121 | if guessed_bom_ref is None: 1122 | custom_print(f"Match not found. I'll create a fake one.") 1123 | components[bom_ref] = create_fake_component(bom_ref) 1124 | else: 1125 | custom_print(f"Match found: {guessed_bom_ref}") 1126 | bom_ref = guessed_bom_ref 1127 | 1128 | if "dependsOn" in dependency: 1129 | for depends_on in dependency["dependsOn"]: 1130 | if depends_on not in components: 1131 | if depends_on in guessed_bom_refs_cache: 1132 | depends_on = guessed_bom_refs_cache[depends_on] 1133 | else: 1134 | custom_print(f"WARNING: 'dependsOn' '{depends_on}' is used in 'dependencies' in a 'dependsOn' field but it's not declared in 'components'. I'll search for a match...") 1135 | guessed_bom_ref = normalize_bom_ref(all_bom_refs, depends_on) 1136 | guessed_bom_refs_cache[depends_on] = guessed_bom_ref 1137 | if guessed_bom_ref is None: 1138 | custom_print(f"Match not found. I'll create a fake one.") 1139 | components[depends_on] = create_fake_component(depends_on) 1140 | else: 1141 | custom_print(f"Match found: {guessed_bom_ref}") 1142 | depends_on = guessed_bom_ref 1143 | 1144 | components[bom_ref]["depends_on"].add(depends_on) 1145 | components[depends_on]["dependency_of"].add(bom_ref) 1146 | 1147 | if "vulnerabilities" in data: 1148 | for vulnerability in data["vulnerabilities"]: 1149 | vuln_id, vuln_severity, vuln_score, vuln_vector = parse_vulnerability_data(vulnerability) 1150 | 1151 | for affects in vulnerability["affects"]: 1152 | bom_ref = affects["ref"] 1153 | if bom_ref not in components: 1154 | custom_print(f"WARNING: 'ref' '{bom_ref}' is used in 'vulnerabilities' but it's not declared in 'components'. I'll create a fake one.") 1155 | components[bom_ref] = create_fake_component(bom_ref) 1156 | 1157 | vulnerability_data = {"id": vuln_id, "severity": vuln_severity, "score": vuln_score, "vector": vuln_vector} 1158 | if vulnerability_data not in components[bom_ref]["vulnerabilities"]: 1159 | components[bom_ref]["vulnerabilities"].append(vulnerability_data) 1160 | if VALID_SEVERITIES[vuln_severity] > VALID_SEVERITIES[components[bom_ref]["max_vulnerability_severity"]]: 1161 | components[bom_ref]["max_vulnerability_severity"] = vuln_severity 1162 | 1163 | return components, metadata_info 1164 | 1165 | 1166 | def parse_string(input_string): 1167 | custom_print("Parsing input string...") 1168 | data = json.loads(input_string) 1169 | return parse_json_data(data) 1170 | 1171 | 1172 | def parse_file(input_file_path): 1173 | custom_print("Parsing input file...") 1174 | with open(input_file_path, 'r') as file: 1175 | data = json.load(file) 1176 | return parse_json_data(data) 1177 | 1178 | 1179 | def prepare_chart_element_name(component): 1180 | if component["version"] != "-": 1181 | name = f'{html.escape(component["name"])} {html.escape(component["version"])}' 1182 | else: 1183 | name = f'{html.escape(component["name"])}' 1184 | 1185 | if len(component["vulnerabilities"]) > 0: 1186 | name += "

Vulnerabilities:
" 1187 | 1188 | vulns = {} 1189 | for vulnerability in component["vulnerabilities"]: 1190 | vulns[f'
  • {html.escape(vulnerability["id"])} ({html.escape(vulnerability["severity"].title())})
  • '] = VALID_SEVERITIES[vulnerability["severity"]] 1191 | 1192 | vulns = dict(sorted(vulns.items(), key=lambda item: (-item[1], item[0]))) 1193 | 1194 | vulns_to_be_shown = list(vulns.keys()) 1195 | if len(vulns_to_be_shown) > 10: 1196 | vulns_to_be_shown = vulns_to_be_shown[:10] 1197 | vulns_to_be_shown.append("
  • ...
  • ") 1198 | 1199 | 1200 | name += "".join(vulns_to_be_shown) 1201 | 1202 | if len(component["license"]) > 0: 1203 | if len(component["vulnerabilities"]) == 0: 1204 | name += "
    " 1205 | name += "
    License:
    " 1206 | 1207 | licenses = [] 1208 | for license in component["license"]: 1209 | licenses.append(f'
  • {html.escape(license)}
  • ') 1210 | 1211 | if len(licenses) > 10: 1212 | licenses = licenses[:10] 1213 | licenses.append("
  • ...
  • ") 1214 | 1215 | name += "".join(licenses) 1216 | 1217 | return name 1218 | 1219 | 1220 | def determine_style(component): 1221 | if component["max_vulnerability_severity"] != "clean": 1222 | return STYLES[component["max_vulnerability_severity"]] 1223 | if component["has_transitive_vulnerabilities"] is True: 1224 | return TRANSITIVE_VULN_STYLE 1225 | else: 1226 | return STYLES[component["max_vulnerability_severity"]] 1227 | 1228 | 1229 | def add_transitive_vulnerabilities_to_component(component, vulnerabilities): 1230 | for vulnerability in vulnerabilities: 1231 | if vulnerability not in component["transitive_vulnerabilities"]: 1232 | component["transitive_vulnerabilities"].append(vulnerability) 1233 | 1234 | 1235 | def format_dependency_chain(parents_branch, depends_on): 1236 | parents_branch.append(depends_on) 1237 | return " --> ".join(parents_branch) 1238 | 1239 | 1240 | def get_children(components, component, parents): 1241 | children = [] 1242 | value = 0 1243 | has_vulnerable_children_or_is_vulnerable = False 1244 | if len(component["vulnerabilities"]) > 0: 1245 | has_vulnerable_children_or_is_vulnerable = True 1246 | for depends_on in component["depends_on"]: 1247 | parents_branch = copy.deepcopy(parents) 1248 | child_name = prepare_chart_element_name(components[depends_on]) 1249 | child_component = components[depends_on] 1250 | child_component["visited"] = True 1251 | if depends_on not in parents_branch: # this is done to avoid infinite recursion in case of circular dependencies 1252 | parents_branch.append(depends_on) 1253 | child_children, children_value, has_vulnerable_children_or_is_vulnerable = get_children(components, child_component, parents_branch) 1254 | if len(child_component["vulnerabilities"]) > 0 or child_component["has_transitive_vulnerabilities"] is True or has_vulnerable_children_or_is_vulnerable is True: 1255 | component["has_transitive_vulnerabilities"] = True 1256 | add_transitive_vulnerabilities_to_component(component, child_component["vulnerabilities"]) 1257 | add_transitive_vulnerabilities_to_component(component, child_component["transitive_vulnerabilities"]) 1258 | has_vulnerable_children_or_is_vulnerable = True 1259 | 1260 | value += children_value 1261 | 1262 | children.append({"name": child_name, 1263 | "children": child_children, 1264 | "value": children_value, 1265 | "itemStyle": determine_style(child_component) 1266 | }) 1267 | else: 1268 | custom_print(f"WARNING: component with bom-ref '{depends_on}' may be a circular dependency. Dependency chain: {format_dependency_chain(parents_branch, depends_on)}") 1269 | value += 1 1270 | for child_depends_on in child_component["depends_on"]: 1271 | child_depends_on = components[child_depends_on] 1272 | if len(child_depends_on["vulnerabilities"]) > 0 or child_depends_on["has_transitive_vulnerabilities"] is True: 1273 | child_component["has_transitive_vulnerabilities"] = True 1274 | add_transitive_vulnerabilities_to_component(child_component, child_depends_on["vulnerabilities"]) 1275 | add_transitive_vulnerabilities_to_component(child_component, child_depends_on["transitive_vulnerabilities"]) 1276 | 1277 | 1278 | children.append({"name": child_name, 1279 | "children": [], 1280 | "value": 1, 1281 | "itemStyle": determine_style(child_component) 1282 | }) 1283 | 1284 | if value == 0: 1285 | value = 1 1286 | 1287 | return children, value, has_vulnerable_children_or_is_vulnerable 1288 | 1289 | 1290 | def add_root_component(components, component, data, bom_ref): 1291 | component["visited"] = True 1292 | parents = [bom_ref] 1293 | root_name = prepare_chart_element_name(component) 1294 | root_children, root_value, has_vulnerable_children_or_is_vulnerable = get_children(components, component, parents) 1295 | 1296 | if has_vulnerable_children_or_is_vulnerable is True: 1297 | component["has_transitive_vulnerabilities"] = True 1298 | for depends_on in component["depends_on"]: 1299 | child = components[depends_on] 1300 | add_transitive_vulnerabilities_to_component(component, child["vulnerabilities"]) 1301 | add_transitive_vulnerabilities_to_component(component, child["transitive_vulnerabilities"]) 1302 | 1303 | new_element = {"name": root_name, 1304 | "children": root_children, 1305 | "value": root_value, 1306 | "itemStyle": determine_style(component) 1307 | } 1308 | data.append(new_element) 1309 | 1310 | 1311 | def build_echarts_data(components): 1312 | data = [] 1313 | 1314 | for bom_ref, component in components.items(): 1315 | if len(component["dependency_of"]) != 0: 1316 | continue 1317 | 1318 | add_root_component(components, component, data, bom_ref) 1319 | 1320 | return data 1321 | 1322 | 1323 | def double_check_if_all_components_were_taken_into_account(components, echart_data): 1324 | # this should happen only for circular dependencies 1325 | for bom_ref, component in components.items(): 1326 | if component["visited"] is False: 1327 | add_root_component(components, component, echart_data, bom_ref) 1328 | 1329 | 1330 | def component_badge_for_table(component): 1331 | component_on_display = "" 1332 | 1333 | if component["max_vulnerability_severity"] == "critical": 1334 | badge_class = 'bg-dark-red' 1335 | elif component["max_vulnerability_severity"] == "high": 1336 | badge_class = 'bg-danger' 1337 | elif component["max_vulnerability_severity"] == "medium": 1338 | badge_class = 'bg-orange' 1339 | elif component["max_vulnerability_severity"] == "low": 1340 | badge_class = 'bg-yellow' 1341 | elif component["max_vulnerability_severity"] in ["information", "info"]: 1342 | badge_class = 'bg-success' 1343 | elif component["max_vulnerability_severity"] == "clean": 1344 | if component["has_transitive_vulnerabilities"]: 1345 | badge_class = 'bg-light-blue' 1346 | else: 1347 | badge_class = 'bg-secondary' 1348 | 1349 | component_on_display += f'' + html.escape(component["name"]) 1350 | if component["version"] != "-": 1351 | component_on_display += " " + html.escape(component["version"]) 1352 | return component_on_display + "" 1353 | 1354 | 1355 | def get_vulnerability_badge_by_severity(severity): 1356 | if severity == "critical": 1357 | return 'bg-dark-red' 1358 | elif severity == "high": 1359 | return 'bg-danger' 1360 | elif severity == "medium": 1361 | return 'bg-orange' 1362 | elif severity == "low": 1363 | return 'bg-yellow' 1364 | elif severity in ["information", "info"]: 1365 | return 'bg-success' 1366 | return '' 1367 | 1368 | def vulnerability_badge_for_table(component, key="vulnerabilities"): 1369 | vulns = {} 1370 | for vulnerability in component[key]: 1371 | badge_class = get_vulnerability_badge_by_severity(vulnerability["severity"]) 1372 | 1373 | vulns[f'{html.escape(vulnerability["severity"].title())} → {html.escape(vulnerability["id"])}'] = VALID_SEVERITIES[vulnerability["severity"]] 1374 | vulns = vulns = dict(sorted(vulns.items(), key=lambda item: (-item[1], item[0]))) 1375 | vulns_to_be_shown = list(vulns.keys()) 1376 | return vulns_to_be_shown 1377 | 1378 | 1379 | def license_badge_for_table(component): 1380 | licenses = [] 1381 | for license in component["license"]: 1382 | licenses.append(f'{html.escape(license)}') 1383 | return licenses 1384 | 1385 | 1386 | def build_components_table_content(components): 1387 | rows = [""" 1388 | 1389 | Component 1390 | Depends on 1391 | Dependency of 1392 | Direct
    vulnerabilities 1393 | Transitive
    vulnerabilities 1394 | License 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | """] 1405 | rows.append("") 1406 | for bom_ref, component in components.items(): 1407 | new_row = "" 1408 | 1409 | new_row += "" + component_badge_for_table(component) + "" 1410 | 1411 | 1412 | if len(component["depends_on"]) == 0: 1413 | new_row += "-" 1414 | else: 1415 | new_row += "" 1416 | depends_on_components_list = [] 1417 | for depends_on in component["depends_on"]: 1418 | component_depends_on = components[depends_on] 1419 | component_depends_on_display = component_badge_for_table(component_depends_on) 1420 | depends_on_components_list.append(component_depends_on_display) 1421 | 1422 | new_row += ',
    '.join(depends_on_components_list) 1423 | new_row += "" 1424 | 1425 | if len(component["dependency_of"]) == 0: 1426 | new_row += "-" 1427 | else: 1428 | new_row += "" 1429 | dependency_of_components_list = [] 1430 | for dependency_of in component["dependency_of"]: 1431 | component_dependency_of = components[dependency_of] 1432 | component_dependency_of_display = component_badge_for_table(component_dependency_of) 1433 | dependency_of_components_list.append(component_dependency_of_display) 1434 | new_row += ',
    '.join(dependency_of_components_list) 1435 | new_row += "" 1436 | 1437 | if len(component["vulnerabilities"]) == 0: 1438 | new_row += "-" 1439 | else: 1440 | vulns_to_be_shown = vulnerability_badge_for_table(component) 1441 | new_row += "" + ',
    '.join(vulns_to_be_shown) + "" 1442 | 1443 | if len(component["transitive_vulnerabilities"]) == 0: 1444 | new_row += "-" 1445 | else: 1446 | vulns_to_be_shown = vulnerability_badge_for_table(component, key="transitive_vulnerabilities") 1447 | new_row += "" + ',
    '.join(vulns_to_be_shown) + "" 1448 | 1449 | if len(component["license"]) == 0: 1450 | new_row += "-" 1451 | else: 1452 | licenses_to_be_shown = license_badge_for_table(component) 1453 | new_row += "" + ',
    '.join(licenses_to_be_shown) + "" 1454 | 1455 | new_row += "\n" 1456 | 1457 | rows.append(new_row) 1458 | 1459 | rows.append("") 1460 | 1461 | return "".join(rows) 1462 | 1463 | 1464 | def is_cve(string): 1465 | pattern = r'^CVE-\d{4}-\d{4,}$' 1466 | return bool(re.match(pattern, string)) 1467 | 1468 | 1469 | def extract_year_and_first_digit(cve_string): 1470 | pattern = r'^CVE-(\d{4})-(\d)(\d*)$' 1471 | match = re.match(pattern, cve_string) 1472 | if match: 1473 | year = match.group(1) 1474 | first_digit = match.group(2) 1475 | return year, first_digit 1476 | else: 1477 | return None, None 1478 | 1479 | 1480 | def get_epss(cve, epss_cache): 1481 | cve = cve.upper().strip() 1482 | 1483 | if not is_cve(cve): 1484 | return "-" 1485 | 1486 | year, first_digit = extract_year_and_first_digit(cve) 1487 | 1488 | cache_key = f"{year}-{first_digit}" 1489 | 1490 | chunk_url = f"https://lucacapacci.github.io/epss/data_groups/epss_scores_{year}_{first_digit}.csv" 1491 | 1492 | if cache_key in epss_cache: 1493 | epss_data = epss_cache[cache_key] 1494 | else: 1495 | custom_print(f"Getting EPSS data from {chunk_url}") 1496 | resp = requests.get(chunk_url) 1497 | 1498 | if resp.status_code == 404: 1499 | epss_cache[cache_key] = None 1500 | return "-" 1501 | 1502 | if resp.status_code != 200: 1503 | epss_cache[cache_key] = None 1504 | custom_print(f"Unexpected status code ({resp.status_code}) for URL {chunk_url}") 1505 | return "-" 1506 | 1507 | epss_data = resp.text 1508 | epss_cache[cache_key] = epss_data 1509 | 1510 | if epss_data is None: 1511 | return "-" 1512 | 1513 | lines = epss_data.splitlines() 1514 | 1515 | headers = [] 1516 | for row in csv.reader(lines[1:]): 1517 | headers = row 1518 | break 1519 | 1520 | for row in csv.reader(lines[2:]): 1521 | if row[headers.index('cve')].upper().strip() == cve: 1522 | decimal_number = Decimal(row[headers.index('epss')]).normalize() 1523 | return f"{decimal_number}" 1524 | 1525 | return "-" 1526 | 1527 | 1528 | def get_cisa_kev(cve, cisa_kev_cache): 1529 | cve = cve.upper().strip() 1530 | 1531 | if not is_cve(cve): 1532 | return "-" 1533 | 1534 | year, first_digit = extract_year_and_first_digit(cve) 1535 | 1536 | cache_key = f"{year}-{first_digit}" 1537 | 1538 | chunk_url = f"https://lucacapacci.github.io/cisa_kev/data_groups/cisa_kev_{year}_{first_digit}.csv" 1539 | 1540 | if cache_key in cisa_kev_cache: 1541 | cisa_kev_data = cisa_kev_cache[cache_key] 1542 | else: 1543 | custom_print(f"Getting CISA KEV data from {chunk_url}") 1544 | resp = requests.get(chunk_url) 1545 | 1546 | if resp.status_code == 404: 1547 | cisa_kev_cache[cache_key] = None 1548 | return "-" 1549 | 1550 | if resp.status_code != 200: 1551 | cisa_kev_cache[cache_key] = None 1552 | custom_print(f"Unexpected status code ({resp.status_code}) for URL {chunk_url}") 1553 | return "-" 1554 | 1555 | cisa_kev_data = resp.text 1556 | cisa_kev_cache[cache_key] = cisa_kev_data 1557 | 1558 | if cisa_kev_data is None: 1559 | return "-" 1560 | 1561 | lines = cisa_kev_data.splitlines() 1562 | 1563 | headers = [] 1564 | for row in csv.reader(lines): 1565 | headers = row 1566 | break 1567 | 1568 | for row in csv.reader(lines[1:]): 1569 | if row[headers.index('cveID')].upper().strip() == cve: 1570 | return row[headers.index('dateAdded')] 1571 | 1572 | return "-" 1573 | 1574 | 1575 | def build_vulnerabilities_table_content(vulnerabilities, components, enrich_cves=False): 1576 | max_epss = "0.0" 1577 | kev_counter = 0 1578 | 1579 | first_row = """ 1580 | 1581 | Vulnerability 1582 | Severity 1583 | Score 1584 | Vector 1585 | """ 1586 | 1587 | if enrich_cves is True: 1588 | first_row += """EPSS 1589 | CISA KEV Date 1590 | """ 1591 | 1592 | first_row += """Directly vulnerable
    components 1593 | Transitively vulnerable
    components 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | """ 1601 | 1602 | if enrich_cves is True: 1603 | first_row += """ 1604 | 1605 | """ 1606 | 1607 | first_row += """ 1608 | 1609 | 1610 | """ 1611 | 1612 | rows = [first_row] 1613 | rows.append("") 1614 | 1615 | epss_cache = {} 1616 | cisa_kev_cache = {} 1617 | 1618 | for _, vulnerability in vulnerabilities.items(): 1619 | rows.append("") 1620 | badge_class = get_vulnerability_badge_by_severity(vulnerability["severity"]) 1621 | rows.append("" + f'{html.escape(vulnerability["id"])}' + "") 1622 | 1623 | rows.append("" + f'{html.escape(vulnerability["severity"].title())}' + "") 1624 | rows.append("" + f'{vulnerability["score"]}' + "") 1625 | rows.append("" + f'{html.escape(vulnerability["vector"])}' + "") 1626 | 1627 | if enrich_cves is True: 1628 | current_epss = get_epss(vulnerability["id"], epss_cache) 1629 | rows.append("" + f'{html.escape(current_epss)}' + "") 1630 | current_cisa_kev = get_cisa_kev(vulnerability["id"], cisa_kev_cache) 1631 | rows.append("" + f'{html.escape(current_cisa_kev)}' + "") 1632 | if current_epss > max_epss: 1633 | max_epss = current_epss 1634 | if current_cisa_kev != "-": 1635 | kev_counter += 1 1636 | 1637 | if len(vulnerability["directly_vulnerable_components"]) == 0: 1638 | rows.append("-") 1639 | else: 1640 | vulnerable_components_td = "" 1641 | content_values = [] 1642 | for component in vulnerability["directly_vulnerable_components"]: 1643 | content_values.append(component_badge_for_table(components[component])) 1644 | vulnerable_components_td += ',
    '.join(content_values) + "" 1645 | rows.append(vulnerable_components_td) 1646 | 1647 | if len(vulnerability["transitively_vulnerable_components"]) == 0: 1648 | rows.append("-") 1649 | else: 1650 | vulnerable_components_td = "" 1651 | content_values = [] 1652 | for component in vulnerability["transitively_vulnerable_components"]: 1653 | content_values.append(component_badge_for_table(components[component])) 1654 | vulnerable_components_td += ',
    '.join(content_values) + "" 1655 | rows.append(vulnerable_components_td) 1656 | 1657 | rows.append("") 1658 | 1659 | rows.append("") 1660 | 1661 | return "".join(rows), max_epss, kev_counter 1662 | 1663 | 1664 | def build_metadata_table_content(metadata_info, counter_critical, counter_high, counter_medium, counter_low, counter_info, components, enrich_cves, max_epss, kev_counter): 1665 | rows = [] 1666 | 1667 | # headers 1668 | rows.append("") 1669 | rows.append("") 1670 | rows.append(f"No. of Components") 1671 | rows.append(f"Vulnerabilities") 1672 | for header, _ in metadata_info.items(): 1673 | rows.append(f"{html.escape(header)}") 1674 | rows.append("") 1675 | rows.append("") 1676 | 1677 | # body 1678 | rows.append("") 1679 | rows.append("") 1680 | 1681 | rows.append(f"{len(components)}") 1682 | 1683 | vulnerabilities_td = "" 1684 | if counter_critical > 0: 1685 | vulnerabilities_td += f'Critical: {counter_critical},  ' 1686 | else: 1687 | vulnerabilities_td += f'Critical: {counter_critical},  ' 1688 | if counter_high > 0: 1689 | vulnerabilities_td += f'High: {counter_high},  ' 1690 | else: 1691 | vulnerabilities_td += f'High: {counter_high},  ' 1692 | if counter_medium > 0: 1693 | vulnerabilities_td += f'Medium: {counter_medium},  ' 1694 | else: 1695 | vulnerabilities_td += f'Medium: {counter_medium},  ' 1696 | if counter_low > 0: 1697 | vulnerabilities_td += f'Low: {counter_low},  ' 1698 | else: 1699 | vulnerabilities_td += f'Low: {counter_low},  ' 1700 | if counter_info > 0: 1701 | vulnerabilities_td += f'Information: {counter_info}' 1702 | else: 1703 | vulnerabilities_td += f'Information: {counter_info}' 1704 | 1705 | if enrich_cves is True: 1706 | vulnerabilities_td += f',
    Max EPSS → {html.escape(max_epss)},
    ' 1707 | vulnerabilities_td += f'Vulnerabilities in CISA KEV → {kev_counter}' 1708 | 1709 | rows.append(f"{vulnerabilities_td}") 1710 | 1711 | for _, metadata_content in metadata_info.items(): 1712 | if isinstance(metadata_content, dict): 1713 | rows.append("") 1714 | content_values = [] 1715 | for content_key, content_value in metadata_content.items(): 1716 | content_values.append(f'{html.escape(content_key)} → {html.escape(content_value)}') 1717 | rows.append(',
    '.join(content_values) + "" 1718 | ) 1719 | rows.append("") 1720 | else: 1721 | rows.append("" + html.escape(f"{metadata_content}") + "") 1722 | 1723 | rows.append("") 1724 | rows.append("") 1725 | 1726 | return "".join(rows) 1727 | 1728 | 1729 | def write_output_file(html_content, output_file_path): 1730 | with open(output_file_path, "w") as text_file: 1731 | text_file.write(html_content) 1732 | 1733 | 1734 | def get_only_vulnerable_components(components): 1735 | vulnerable_components = {} 1736 | 1737 | # populate vulnerable components 1738 | for component_bom_ref, component in components.items(): 1739 | if len(component["vulnerabilities"]) == 0 and len(component["transitive_vulnerabilities"]) == 0: 1740 | continue # component is not vulnerable in any way 1741 | 1742 | vulnerable_component = {"name": component["name"], 1743 | "version": component["version"], 1744 | "type": component["type"], 1745 | "license": copy.deepcopy(component["license"]), 1746 | "depends_on": copy.deepcopy(component["depends_on"]), 1747 | "dependency_of": copy.deepcopy(component["dependency_of"]), 1748 | "vulnerabilities": copy.deepcopy(component["vulnerabilities"]), 1749 | "transitive_vulnerabilities": copy.deepcopy(component["transitive_vulnerabilities"]), 1750 | "max_vulnerability_severity": component["max_vulnerability_severity"], 1751 | "has_transitive_vulnerabilities": component["has_transitive_vulnerabilities"], 1752 | "visited": False} 1753 | vulnerable_components[component_bom_ref] = vulnerable_component 1754 | 1755 | # clean not vulnerable dependency relationships 1756 | vulnerable_components_bom_refs = set(vulnerable_components.keys()) 1757 | for component_bom_ref, component in vulnerable_components.items(): 1758 | vulnerable_depends_on = set(component["depends_on"]) & vulnerable_components_bom_refs 1759 | component["depends_on"] = vulnerable_depends_on 1760 | vulnerable_dependency_of = set(component["dependency_of"]) & vulnerable_components_bom_refs 1761 | component["dependency_of"] = vulnerable_dependency_of 1762 | 1763 | return vulnerable_components 1764 | 1765 | 1766 | def parse_vulnerabilities(components): 1767 | vulnerabilities = {} 1768 | 1769 | counter_critical = 0 1770 | counter_high = 0 1771 | counter_medium = 0 1772 | counter_low = 0 1773 | counter_info = 0 1774 | 1775 | # populate vulnerable components 1776 | for component_bom_ref, component in components.items(): 1777 | if len(component["vulnerabilities"]) == 0 and len(component["transitive_vulnerabilities"]) == 0: 1778 | continue # component is not vulnerable in any way 1779 | 1780 | for vulnerability in component["vulnerabilities"]: 1781 | vuln_key = f"{vulnerability['id']}-{vulnerability['severity']}-{vulnerability['score']}" 1782 | 1783 | if vuln_key not in vulnerabilities: 1784 | vulnerabilities[vuln_key] = {"id": vulnerability['id'], 1785 | "severity": vulnerability['severity'], 1786 | "score": vulnerability['score'], 1787 | "vector": vulnerability['vector'], 1788 | "directly_vulnerable_components": set(), 1789 | "transitively_vulnerable_components": set()} 1790 | 1791 | if vulnerability['severity'] == "critical": 1792 | counter_critical += 1 1793 | elif vulnerability['severity'] == "high": 1794 | counter_high += 1 1795 | elif vulnerability['severity'] == "medium": 1796 | counter_medium += 1 1797 | elif vulnerability['severity'] == "low": 1798 | counter_low += 1 1799 | else: 1800 | counter_info += 1 1801 | 1802 | vulnerabilities[vuln_key]["directly_vulnerable_components"].add(component_bom_ref) 1803 | 1804 | for vulnerability in component["transitive_vulnerabilities"]: 1805 | vuln_key = f"{vulnerability['id']}-{vulnerability['severity']}-{vulnerability['score']}" 1806 | 1807 | if vuln_key not in vulnerabilities: 1808 | vulnerabilities[vuln_key] = {"id": vulnerability['id'], 1809 | "severity": vulnerability['severity'], 1810 | "score": vulnerability['score'], 1811 | "vector": vulnerability['vector'], 1812 | "directly_vulnerable_components": set(), 1813 | "transitively_vulnerable_components": set()} 1814 | 1815 | if vulnerability['severity'] == "critical": 1816 | counter_critical += 1 1817 | elif vulnerability['severity'] == "high": 1818 | counter_high += 1 1819 | elif vulnerability['severity'] == "medium": 1820 | counter_medium += 1 1821 | elif vulnerability['severity'] == "low": 1822 | counter_low += 1 1823 | else: 1824 | counter_info += 1 1825 | 1826 | vulnerabilities[vuln_key]["transitively_vulnerable_components"].add(component_bom_ref) 1827 | 1828 | 1829 | return vulnerabilities, counter_critical, counter_high, counter_medium, counter_low, counter_info 1830 | 1831 | 1832 | def main_cli(input_file_path, output_file_path, enrich_cves): 1833 | if not os.path.exists(input_file_path): 1834 | custom_print(f"File does not exist: '{input_file_path}'") 1835 | exit() 1836 | 1837 | try: 1838 | components, metadata_info = parse_file(input_file_path) 1839 | except Exception as e: 1840 | custom_print(f"Error parsing input file: {e}") 1841 | exit() 1842 | 1843 | # chart with all components 1844 | echart_data_all_components = build_echarts_data(components) 1845 | double_check_if_all_components_were_taken_into_account(components, echart_data_all_components) 1846 | 1847 | vulnerabilities, counter_critical, counter_high, counter_medium, counter_low, counter_info = parse_vulnerabilities(components) 1848 | 1849 | # chart with only vulnerable components 1850 | vulnerable_components = get_only_vulnerable_components(components) 1851 | echart_data_vulnerable_components = build_echarts_data(vulnerable_components) 1852 | double_check_if_all_components_were_taken_into_account(vulnerable_components, echart_data_vulnerable_components) 1853 | 1854 | components_table_content = build_components_table_content(components) 1855 | vulnerabilities_table_content, max_epss, kev_counter = build_vulnerabilities_table_content(vulnerabilities, components, enrich_cves) 1856 | metadata_table_content = build_metadata_table_content(metadata_info, counter_critical, counter_high, counter_medium, counter_low, counter_info, components, enrich_cves, max_epss, kev_counter) 1857 | 1858 | html_content = HTML_TEMPLATE.replace("", json.dumps(echart_data_all_components, indent=2)) 1859 | html_content = html_content.replace("", json.dumps(echart_data_vulnerable_components, indent=2)) 1860 | html_content = html_content.replace("", html.escape(os.path.basename(input_file_path))) 1861 | html_content = html_content.replace("", components_table_content) 1862 | html_content = html_content.replace("", vulnerabilities_table_content) 1863 | html_content = html_content.replace("", metadata_table_content) 1864 | 1865 | write_output_file(html_content, output_file_path) 1866 | custom_print("Done.") 1867 | 1868 | 1869 | def main_web(input_string, enrich_cves): 1870 | try: 1871 | components, metadata_info = parse_string(input_string) 1872 | except Exception as e: 1873 | custom_print(f"Error parsing input string: {e}") 1874 | exit() 1875 | 1876 | # chart with all components 1877 | echart_data_all_components = build_echarts_data(components) 1878 | double_check_if_all_components_were_taken_into_account(components, echart_data_all_components) 1879 | echart_data_all_components = json.dumps(echart_data_all_components, indent=2) 1880 | 1881 | vulnerabilities, counter_critical, counter_high, counter_medium, counter_low, counter_info = parse_vulnerabilities(components) 1882 | 1883 | # chart with only vulnerable components 1884 | vulnerable_components = get_only_vulnerable_components(components) 1885 | echart_data_vulnerable_components = build_echarts_data(vulnerable_components) 1886 | double_check_if_all_components_were_taken_into_account(vulnerable_components, echart_data_vulnerable_components) 1887 | echart_data_vulnerable_components = json.dumps(echart_data_vulnerable_components, indent=2) 1888 | 1889 | components_table_content = build_components_table_content(components) 1890 | vulnerabilities_table_content, max_epss, kev_counter = build_vulnerabilities_table_content(vulnerabilities, components, enrich_cves) 1891 | metadata_table_content = build_metadata_table_content(metadata_info, counter_critical, counter_high, counter_medium, counter_low, counter_info, components, enrich_cves, max_epss, kev_counter) 1892 | 1893 | return echart_data_all_components, echart_data_vulnerable_components, components_table_content, metadata_table_content, vulnerabilities_table_content 1894 | 1895 | 1896 | if __name__ == "__main__": 1897 | custom_print(f''' 1898 | ▗▄▄▖▗▖ ▗▖▗▖ ▗▖ ▗▄▄▖▗▖ ▗▖▗▄▄▄▖▗▖ ▗▖▗▄▄▄▖ 1899 | ▐▌ ▐▌ ▐▌▐▛▚▖▐▌▐▌ ▐▌ ▐▌ █ ▐▛▚▖▐▌▐▌ 1900 | ▝▀▚▖▐▌ ▐▌▐▌ ▝▜▌ ▝▀▚▖▐▛▀▜▌ █ ▐▌ ▝▜▌▐▛▀▀▘ 1901 | ▗▄▄▞▘▝▚▄▞▘▐▌ ▐▌▗▄▄▞▘▐▌ ▐▌▗▄█▄▖▐▌ ▐▌▐▙▄▄▖ v{VERSION} 1902 | ''') 1903 | 1904 | parser = argparse.ArgumentParser(description=f"{NAME}: actionable CycloneDX visualization") 1905 | parser.add_argument("-v", "--version", help="show program version", action="store_true") 1906 | parser.add_argument("-i", "--input", help="path of input CycloneDX file") 1907 | parser.add_argument("-o", "--output", help="path of output HTML file") 1908 | parser.add_argument("-e", "--enrich", help="enrich CVEs with EPSS and CISA KEV", action="store_true") 1909 | args = parser.parse_args() 1910 | 1911 | if args.version: 1912 | exit() 1913 | 1914 | if not args.input or not args.output: 1915 | parser.print_help() 1916 | exit() 1917 | 1918 | input_file_path = args.input 1919 | output_file_path = args.output 1920 | 1921 | enrich_cves = False 1922 | if args.enrich: 1923 | enrich_cves = True 1924 | 1925 | main_cli(input_file_path, output_file_path, enrich_cves) 1926 | 1927 | 1928 | if __name__ == "__web__": 1929 | echart_data_all_components, echart_data_vulnerable_components, components_table_content, metadata_table_content, vulnerabilities_table_content = main_web(INPUT_DATA, DO_ENRICHMENT) 1930 | OUTPUT_CHART_DATA = echart_data_all_components 1931 | OUTPUT_CHART_DATA_VULNERABLE_COMPONENTS = echart_data_vulnerable_components 1932 | OUTPUT_COMPONENTS_TABLE_DATA = components_table_content 1933 | OUTPUT_METADATA_TABLE_DATA = metadata_table_content 1934 | OUTPUT_VULNERABILITIES_TABLE_DATA = vulnerabilities_table_content 1935 | 1936 | --------------------------------------------------------------------------------