diff --git a/.vscode/launch.json b/.vscode/launch.json index ff66a56..041db43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -40,14 +40,6 @@ "mode": "debug", "program": "${workspaceFolder}", "args": ["image", "--scanners", "vuln", "ruby:3.1", "interactive_report.html"] - }, - { - "name": "Debug scan2htmlBash", - "type": "bashdb", - "request": "launch", - "program": "${workspaceFolder}/scan2html", - "args": ["trivy", "scan2html", "image", "--format", "spdx", "ghcr.io/zalando/spilo-15:3.0-p1", "test-report.html"] - //"args": ["test/assets/app-template-test.html", "test/data/default/results.json", "test-report.html"] } ] } diff --git a/README.md b/README.md index a7d80e9..4c1b5b4 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,10 @@ Examples: # Scan and generate SBOM(spdx) report trivy scan2html image --format spdx alpine:3.15 --scan2html-flags --output interactive_report.html - # Generate a report from multiple json scan results - experimental + # Generate a report from multiple json scan results trivy scan2html generate --scan2html-flags --output interactive_report.html --from vulnerabilities.json,misconfigs.json,secrets.json - # Generate report with EPSS scores from multiple scan results - experimental + # Generate report with EPSS scores from multiple scan results trivy scan2html generate --scan2html-flags --with-epss --output interactive_report.html --from vulnerabilities.json,misconfigs.json,secrets.json ``` diff --git a/internal/common/flags.go b/internal/common/flags.go index e89843a..7ffc87b 100644 --- a/internal/common/flags.go +++ b/internal/common/flags.go @@ -2,8 +2,8 @@ package common import ( "fmt" - "scan2html/internal/logger" "os" + "scan2html/internal/logger" ) var AvailableFlags = map[string]bool{ @@ -11,6 +11,7 @@ var AvailableFlags = map[string]bool{ "--scan2html-flags": true, "--output": false, "--with-epss": true, + "--with-exploits": true, "--report-title": false, "generate": true, "--from": false, diff --git a/internal/exploit/downloader.go b/internal/exploit/downloader.go new file mode 100644 index 0000000..6791a5f --- /dev/null +++ b/internal/exploit/downloader.go @@ -0,0 +1,32 @@ +package exploit + +import ( + "fmt" + "os" + "path/filepath" + "scan2html/internal/epss" + "scan2html/internal/logger" +) + +// PrepareExploitData downloads the CISA dataset, saves it as a temporary file, +func PrepareExploitData() (string, error) { + const ( + cisaURL = "https://www.cisa.gov/sites/default/files/feeds" + cisaFileName = "known_exploited_vulnerabilities.json" + ) + + // Define paths + tmpCisaFilepath := filepath.Join(os.TempDir(), cisaFileName) + cisaDownloadUrl := fmt.Sprintf("%s/%s", cisaURL, cisaFileName) + logger.Logger.Infof("Downloading Exploit data from: %s\n", cisaDownloadUrl) + + if err := epss.DownloadFile(cisaDownloadUrl, tmpCisaFilepath); err != nil { + return "", err + } + logger.Logger.Infof("Exploit data downloaded to: %s\n", tmpCisaFilepath) + + stats, _ := os.Stat(tmpCisaFilepath) + logger.Logger.Infof("File decompressed successfully to %s with size of: %d bytes\n", tmpCisaFilepath, stats.Size()) + + return tmpCisaFilepath, nil +} diff --git a/internal/report/generator.go b/internal/report/generator.go index f48e256..caba288 100644 --- a/internal/report/generator.go +++ b/internal/report/generator.go @@ -9,6 +9,7 @@ import ( "path/filepath" "scan2html/internal/common" "scan2html/internal/epss" + "scan2html/internal/exploit" "scan2html/internal/logger" "strings" "time" @@ -25,10 +26,12 @@ func GenerateHtmlReport(pluginFlags common.Flags, version string) error { reportName := pluginFlags["--output"] _, withEpss := pluginFlags["--with-epss"] + _, withExploits := pluginFlags["--with-exploits"] reportTitle := pluginFlags["--report-title"] // Log input parameters for clarity logger.Logger.Infof("Base Directory: %s\n", baseDir) logger.Logger.Infof("With EPSS: %t\n", withEpss) + logger.Logger.Infof("With Exploits: %t\n", withExploits) logger.Logger.Infof("Report Title: %s\n", reportTitle) logger.Logger.Infof("Report Name: %s\n", reportName) @@ -57,26 +60,58 @@ func GenerateHtmlReport(pluginFlags common.Flags, version string) error { } // Handle EPSS data if enabled + // replaceTextByFile "$report_name" "\"TEMP_EPSS_DATA\"" "$epss_data" + // Schedule deletion of the EPSS data file upon function exit + shouldReturn, returnValue := handleEPSS(withEpss, reportName) + if shouldReturn { + return returnValue + } + + shouldReturn, returnValue = handleExploit(withExploits, reportName) + if shouldReturn { + return returnValue + } + + logger.Logger.Infof("%s has been created successfully!\n", reportName) + return nil +} + +func handleEPSS(withEpss bool, reportName string) (bool, error) { if withEpss { logger.Logger.Infoln("EPSS enabled!") var epssDataFile, err = epss.PrepareEpssData() if err != nil { - return fmt.Errorf("failed to prepare EPSS data: %v", err) + return true, fmt.Errorf("failed to prepare EPSS data: %v", err) } - // replaceTextByFile "$report_name" "\"TEMP_EPSS_DATA\"" "$epss_data" if err := replaceTextByFile(reportName, "\"TEMP_EPSS_DATA\"", epssDataFile); err != nil { - return fmt.Errorf("failed to replace EPSS data in %s: %v", reportName, err) + return true, fmt.Errorf("failed to replace EPSS data in %s: %v", reportName, err) } logger.Logger.Infoln("EPSS data imported!") - // Schedule deletion of the EPSS data file upon function exit defer os.Remove(epssDataFile) } + return false, nil +} - logger.Logger.Infof("%s has been created successfully!\n", reportName) - return nil +func handleExploit(withExploits bool, reportName string) (bool, error) { + if withExploits { + logger.Logger.Infoln("Exploits enabled!") + var exploitDataFile, err = exploit.PrepareExploitData() + if err != nil { + return true, fmt.Errorf("failed to prepare Exploits data: %v", err) + } + + if err := replaceTextByFile(reportName, "{TEMP_EXPLOITS:0}", exploitDataFile); err != nil { + return true, fmt.Errorf("failed to replace Exploits data in %s: %v", reportName, err) + } + + logger.Logger.Infoln("Exploits data imported!") + + defer os.Remove(exploitDataFile) + } + return false, nil } // replaceTextByText replaces occurrences of search_text in the input file with replace_content. @@ -119,36 +154,35 @@ func replaceTextByText(inputFile, searchText, replaceContent string) error { return fmt.Errorf("error writing to temp file: %v", err) } - return copyAndRemove(tempFile.Name(), inputFile) } func copyAndRemove(src, dst string) error { - // Open the source file - sourceFile, err := os.Open(src) - if err != nil { - return err - } - defer sourceFile.Close() - - // Create the destination file - destFile, err := os.Create(dst) - if err != nil { - return err - } - defer destFile.Close() - - // Copy the contents - if _, err := io.Copy(destFile, sourceFile); err != nil { - return err - } - - // Close files before removal - sourceFile.Close() - destFile.Close() - - // Remove the source file - return os.Remove(src) + // Open the source file + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + // Create the destination file + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + // Copy the contents + if _, err := io.Copy(destFile, sourceFile); err != nil { + return err + } + + // Close files before removal + sourceFile.Close() + destFile.Close() + + // Remove the source file + return os.Remove(src) } // replaceTextByFile replaces occurrences of search_text in the input file with content from replace_file. diff --git a/release-candidate/plugin.yaml b/release-candidate/plugin.yaml index b772e3a..4e4cdd4 100644 --- a/release-candidate/plugin.yaml +++ b/release-candidate/plugin.yaml @@ -1,5 +1,5 @@ name: "scan2html" -version: "0.3.15-rc.1" +version: "0.3.16-rc.1" maintainer: fatihtokus repository: github.com/fatihtokus/scan2html summary: A Trivy plugin that scans and outputs the results to a single page app. @@ -12,58 +12,58 @@ platforms: - selector: os: linux arch: amd64 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_linux-amd64.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_linux-amd64.tar.gz bin: ./scan2html - selector: os: linux arch: arm - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_linux-arm.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_linux-arm.tar.gz bin: ./scan2html - selector: os: linux arch: arm64 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_linux-arm64.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_linux-arm64.tar.gz bin: ./scan2html - selector: os: linux arch: s390x - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_linux-s390x.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_linux-s390x.tar.gz bin: ./scan2html - selector: os: linux arch: ppc64le - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_linux-ppc64le.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_linux-ppc64le.tar.gz bin: ./scan2html - selector: os: linux arch: 386 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_linux-386.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_linux-386.tar.gz bin: ./scan2html - selector: os: darwin arch: amd64 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_darwin-amd64.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_darwin-amd64.tar.gz bin: ./scan2html - selector: os: darwin arch: arm64 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_darwin-arm64.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_darwin-arm64.tar.gz bin: ./scan2html - selector: os: freebsd arch: 386 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_freebsd-386.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_freebsd-386.tar.gz bin: ./scan2html - selector: os: freebsd arch: amd64 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_freebsd-amd64.tar.gz + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_freebsd-amd64.tar.gz bin: ./scan2html - selector: os: windows arch: amd64 - uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.15-rc.1/scan2html_0.3.15-rc.1_windows-amd64.zip + uri: https://github.com/fatihtokus/scan2html/releases/download/v0.3.16-rc.1/scan2html_0.3.16-rc.1_windows-amd64.zip bin: ./scan2html \ No newline at end of file diff --git a/src/frontend-app/src/App.tsx b/src/frontend-app/src/App.tsx index 8faf8a7..0f58005 100644 --- a/src/frontend-app/src/App.tsx +++ b/src/frontend-app/src/App.tsx @@ -4,15 +4,20 @@ import Papa, { ParseResult } from "papaparse"; import TrivyReport from "./components/trivy-report/TrivyReport"; import TableTitle from "./components/shared/TableTitle"; import defaultData from "./data/results.json"; +import knownExploitedVulnerabilitiesData from "./data/cisa-known-exploited-vulnerabilities.json"; import defaultEPSSData from "./data/epss.cvs?raw"; import defaultResultMetaData from "./data/result-metadata.json"; import { NormalizedResultForDataTable, UploadInfo } from "./types"; import { EPSSPerVulnerability } from "./types/external/epss"; +import { CisaExploit } from "./types/external/cisaExploit"; import { getSecrets, getLicenses, getMisconfigurationSummary, getK8sClusterSummaryForInfraAssessment, getK8sClusterSummaryForRBACAssessment, getMisconfigurations, getVulnerabilities, getSupplyChainSBOM } from "./utils/index"; import { FileProtectOutlined, UploadOutlined, LockOutlined, ExclamationCircleOutlined, SettingOutlined, ClusterOutlined, ProfileOutlined, MenuFoldOutlined, MenuUnfoldOutlined, BugOutlined } from "@ant-design/icons"; import "./App.css"; import type { MenuProps } from "antd"; + +const knownExploitedVulnerabilities: CisaExploit[] = knownExploitedVulnerabilitiesData as CisaExploit[]; + type MenuItem = { key: string; icon: any; @@ -112,7 +117,7 @@ function App() { useEffect(() => { console.log(`loadedData.length ${loadedData.length}`); - setVulnerabilities(getVulnerabilities(loadedData, epssData)); + setVulnerabilities(getVulnerabilities(loadedData, epssData, knownExploitedVulnerabilities)); setSecrets(getSecrets(loadedData)); setLicenses(getLicenses(loadedData)); setMisconfigurations(getMisconfigurations(loadedData)); diff --git a/src/frontend-app/src/components/shared/Exploit.tsx b/src/frontend-app/src/components/shared/Exploit.tsx new file mode 100644 index 0000000..633683a --- /dev/null +++ b/src/frontend-app/src/components/shared/Exploit.tsx @@ -0,0 +1,32 @@ + +import { Tag, Tooltip } from "antd"; +import { SafetyCertificateOutlined, LinkOutlined } from "@ant-design/icons"; + +const Exploit = ({ vulnerabilityID }: { vulnerabilityID: string }) => { + return ( +
+ + } + style={{ + display: "flex", + alignItems: "center", + borderRadius: "-2px", + cursor: "pointer", + fontSize: "10px", + backgroundColor: "#FFA500", + color: '#000', // Set text color to black + fontWeight: "bold", // Make text bold + }} + + onClick={() => window.open("https://nvd.nist.gov/vuln/detail/" + vulnerabilityID, "_blank")} + > + CISA KEV + + + +
+ ); +}; + +export default Exploit; \ No newline at end of file diff --git a/src/frontend-app/src/components/trivy-report/Vulnerabilities.tsx b/src/frontend-app/src/components/trivy-report/Vulnerabilities.tsx index 5e21343..b4ceaf3 100644 --- a/src/frontend-app/src/components/trivy-report/Vulnerabilities.tsx +++ b/src/frontend-app/src/components/trivy-report/Vulnerabilities.tsx @@ -13,6 +13,7 @@ import { isNegligible } from '../shared/SeverityTag'; import SeverityTag from '../shared/SeverityTag'; import { severityFilters } from '../../constants'; import SeverityToolbar from '../shared/SeverityToolbar'; +import Exploit from '../shared/Exploit'; import dayjs from 'dayjs'; interface VulnerabilitiesProps { @@ -190,6 +191,16 @@ const Vulnerabilities: React.FC = ({ result }) => { sorter: (a: NormalizedResultForDataTable, b: NormalizedResultForDataTable) => severityCompare(a.Severity, b.Severity), sortDirections: ['descend', 'ascend'], }, + { + title: 'Exploits', + dataIndex: 'Exploits', + key: 'Exploits', + width: '5%', + ...getColumnSearchProps('Exploits'), + sorter: (a: NormalizedResultForDataTable, b: NormalizedResultForDataTable) => numberCompare(a.Exploits, b.Exploits), + sortDirections: ['descend', 'ascend'], + render: (exploits, vulnerability) => exploits == 'CISA' && , + }, { title: 'Installed Version', dataIndex: 'InstalledVersion', @@ -220,30 +231,6 @@ const Vulnerabilities: React.FC = ({ result }) => { sorter: (a: NormalizedResultForDataTable, b: NormalizedResultForDataTable) => localeCompare(a.Title, b.Title), sortDirections: ['descend', 'ascend'], }, - { - title: 'Published Date', - dataIndex: 'PublishedDate', - key: 'PublishedDate', - width: '5%', - ...getColumnSearchProps('PublishedDate'), - sorter: (a: NormalizedResultForDataTable, b: NormalizedResultForDataTable) => localeCompare(a.PublishedDate, b.PublishedDate), - sortDirections: ['descend', 'ascend'], - render:(PublishedDate)=> ( - dayjs(PublishedDate).format('YYYY-MM-DD') - ), - }, - { - title: 'Last Modified Date', - dataIndex: 'LastModifiedDate', - key: 'LastModifiedDate', - width: '5%', - ...getColumnSearchProps('LastModifiedDate'), - sorter: (a: NormalizedResultForDataTable, b: NormalizedResultForDataTable) => localeCompare(a.LastModifiedDate, b.LastModifiedDate), - sortDirections: ['descend', 'ascend'], - render:(LastModifiedDate)=> ( - dayjs(LastModifiedDate).format('YYYY-MM-DD') - ), - }, ]; const handleExpand = (expanded: boolean, record: NormalizedResultForDataTable) => { @@ -266,7 +253,8 @@ const Vulnerabilities: React.FC = ({ result }) => { expandable={{ expandedRowRender: (vulnerability) => (
- Description: {vulnerability.Description} + Description: {vulnerability.Description}
+ Published Date: {dayjs(vulnerability.PublishedDate).format('YYYY-MM-DD')} Last Modified Dates: {dayjs(vulnerability.LastModifiedDate).format('YYYY-MM-DD')}

References:

    {vulnerability.References?.map((ref, index) => ( diff --git a/src/frontend-app/src/data/cisa-known-exploited-vulnerabilities.json b/src/frontend-app/src/data/cisa-known-exploited-vulnerabilities.json new file mode 100644 index 0000000..9abbb04 --- /dev/null +++ b/src/frontend-app/src/data/cisa-known-exploited-vulnerabilities.json @@ -0,0 +1,3 @@ +[ + {"TEMP_EXPLOITS": 0} +] \ No newline at end of file diff --git a/src/frontend-app/src/types/external/cisaExploit.ts b/src/frontend-app/src/types/external/cisaExploit.ts new file mode 100644 index 0000000..e2ead92 --- /dev/null +++ b/src/frontend-app/src/types/external/cisaExploit.ts @@ -0,0 +1,12 @@ +export type CisaExploit = { + title: string; + catalogVersion: string; + count: number; + vulnerabilities: Exploit[] +}; + +export type Exploit = { + cveID: string; + vendorProject: string; +}; + diff --git a/src/frontend-app/src/types/index.ts b/src/frontend-app/src/types/index.ts index 5ff3010..3755133 100644 --- a/src/frontend-app/src/types/index.ts +++ b/src/frontend-app/src/types/index.ts @@ -7,6 +7,7 @@ export class NormalizedResultForDataTable { NVD_V2Score?: number; NVD_V3Score?: number; EPSS_Score?: number; + Exploits?: string; Severity?: string; InstalledVersion?: string; FixedVersion?: string; diff --git a/src/frontend-app/src/utils/index.tsx b/src/frontend-app/src/utils/index.tsx index 5ac467c..d5fb590 100644 --- a/src/frontend-app/src/utils/index.tsx +++ b/src/frontend-app/src/utils/index.tsx @@ -1,4 +1,5 @@ import { NormalizedResultForDataTable } from "../types"; +import { CisaExploit } from "../types/external/cisaExploit"; import { CommonScanResult, CommonResult, Holder } from "../types/external/defaultResult"; export function removeDuplicateResults(results: NormalizedResultForDataTable[]) @@ -12,7 +13,7 @@ export function removeDuplicateResults(results: NormalizedResultForDataTable[]) }; export function getVulnerabilities(results: any[] //CommonScanResult[] - , epssData: any[] + , epssData: any[], knownExloitedVulnerabilitiesData: CisaExploit[] ): NormalizedResultForDataTable[] { if (results === undefined) { return []; @@ -26,7 +27,15 @@ export function getVulnerabilities(results: any[] //CommonScanResult[] } }); - return enrichWithEPSSCores(formattedResultJson, epssData); + if (epssData.length > 0) { + formattedResultJson = enrichWithEPSSCores(formattedResultJson, epssData); + } + + if (knownExloitedVulnerabilitiesData.length > 0 && knownExloitedVulnerabilitiesData[0].count > 0) { + formattedResultJson = enrichWithEPSSCores1(formattedResultJson, knownExloitedVulnerabilitiesData[0]); + } + + return formattedResultJson; } function enrichWithEPSSCores(vulnerabilities: NormalizedResultForDataTable[], epssData: any[] @@ -41,6 +50,18 @@ function enrichWithEPSSCores(vulnerabilities: NormalizedResultForDataTable[], ep return vulnerabilities; } +function enrichWithEPSSCores1(vulnerabilities: NormalizedResultForDataTable[], knownExloitedVulnerabilitiesData: CisaExploit +): NormalizedResultForDataTable[] { + vulnerabilities.forEach(vulnerability => { + let EPSS_Score = knownExloitedVulnerabilitiesData.vulnerabilities.filter(epssPerVulnerability => epssPerVulnerability.cveID === vulnerability.ID)[0]; + if (EPSS_Score) { + vulnerability.Exploits = "CISA"; + } + }); + + return vulnerabilities; +} + function getVulnerabilitiesFromAReport( results: any //CommonScanResult ): NormalizedResultForDataTable[] {