Repository: anchore/grype Branch: main Commit: dee8de483dfb Files: 1026 Total size: 4.6 MB Directory structure: gitextract_p7zkpam3/ ├── .binny.yaml ├── .bouncer.yaml ├── .chronicle.yaml ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── match_issue.md │ ├── actions/ │ │ └── bootstrap/ │ │ └── action.yaml │ ├── dependabot.yaml │ ├── scripts/ │ │ ├── check-syft-version-is-release.sh │ │ ├── ci-check.sh │ │ ├── coverage.py │ │ ├── db-schema-drift-check.sh │ │ ├── go-mod-tidy-check.sh │ │ ├── json-schema-drift-check.sh │ │ └── trigger-release.sh │ ├── workflows/ │ │ ├── codeql-analysis.yml │ │ ├── dependabot-automation.yaml │ │ ├── oss-project-board-add.yaml │ │ ├── release.yaml │ │ ├── remove-awaiting-response-label.yaml │ │ ├── scorecards.yml │ │ ├── update-anchore-dependencies.yml │ │ ├── update-bootstrap-tools.yml │ │ ├── update-generated-code.yml │ │ ├── update-quality-gate-db.yml │ │ ├── validate-github-actions.yaml │ │ └── validations.yaml │ └── zizmor.yml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .goreleaser.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.debug ├── Dockerfile.nonroot ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── SECURITY.md ├── Taskfile.yaml ├── artifacthub-repo.yml ├── cmd/ │ └── grype/ │ ├── cli/ │ │ ├── cli.go │ │ ├── cli_test.go │ │ ├── commands/ │ │ │ ├── completion.go │ │ │ ├── db.go │ │ │ ├── db_check.go │ │ │ ├── db_check_test.go │ │ │ ├── db_delete.go │ │ │ ├── db_import.go │ │ │ ├── db_list.go │ │ │ ├── db_list_test.go │ │ │ ├── db_providers.go │ │ │ ├── db_providers_test.go │ │ │ ├── db_search.go │ │ │ ├── db_search_test.go │ │ │ ├── db_search_vuln.go │ │ │ ├── db_search_vuln_test.go │ │ │ ├── db_status.go │ │ │ ├── db_status_test.go │ │ │ ├── db_update.go │ │ │ ├── explain.go │ │ │ ├── internal/ │ │ │ │ ├── dbsearch/ │ │ │ │ │ ├── affected_packages.go │ │ │ │ │ ├── affected_packages_test.go │ │ │ │ │ ├── common.go │ │ │ │ │ ├── matches.go │ │ │ │ │ ├── matches_test.go │ │ │ │ │ ├── versions.go │ │ │ │ │ ├── vulnerabilities.go │ │ │ │ │ ├── vulnerabilities_test.go │ │ │ │ │ └── vulnerability_decorations.go │ │ │ │ └── jsonschema/ │ │ │ │ └── main.go │ │ │ ├── root.go │ │ │ ├── root_test.go │ │ │ ├── testdata/ │ │ │ │ └── provider-metadata.json │ │ │ ├── update.go │ │ │ ├── update_test.go │ │ │ ├── util.go │ │ │ └── util_test.go │ │ ├── options/ │ │ │ ├── alerts.go │ │ │ ├── alerts_test.go │ │ │ ├── database.go │ │ │ ├── database_command.go │ │ │ ├── database_search_bounds.go │ │ │ ├── database_search_format.go │ │ │ ├── database_search_os.go │ │ │ ├── database_search_os_test.go │ │ │ ├── database_search_packages.go │ │ │ ├── database_search_packages_test.go │ │ │ ├── database_search_vulnerabilities.go │ │ │ ├── database_search_vulnerabilities_test.go │ │ │ ├── datasources.go │ │ │ ├── experimental.go │ │ │ ├── fix_channels.go │ │ │ ├── grype.go │ │ │ ├── grype_test.go │ │ │ ├── match.go │ │ │ ├── match_test.go │ │ │ ├── registry.go │ │ │ ├── registry_test.go │ │ │ ├── search.go │ │ │ ├── secret.go │ │ │ └── sort_by.go │ │ └── ui/ │ │ ├── __snapshots__/ │ │ │ ├── handle_database_diff_started_test.snap │ │ │ ├── handle_update_vulnerability_database_test.snap │ │ │ └── handle_vulnerability_scanning_started_test.snap │ │ ├── handle_database_diff_started.go │ │ ├── handle_database_diff_started_test.go │ │ ├── handle_update_vulnerability_database.go │ │ ├── handle_update_vulnerability_database_test.go │ │ ├── handle_vulnerability_scanning_started.go │ │ ├── handle_vulnerability_scanning_started_test.go │ │ ├── handler.go │ │ ├── new_task_progress.go │ │ └── util_test.go │ ├── internal/ │ │ ├── constants.go │ │ └── ui/ │ │ ├── __snapshots__/ │ │ │ └── post_ui_event_writer_test.snap │ │ ├── no_ui.go │ │ ├── post_ui_event_writer.go │ │ ├── post_ui_event_writer_test.go │ │ └── ui.go │ └── main.go ├── go.mod ├── go.sum ├── grype/ │ ├── cpe/ │ │ ├── cpe.go │ │ └── cpe_test.go │ ├── db/ │ │ ├── build.go │ │ ├── data/ │ │ │ ├── entry.go │ │ │ ├── processor.go │ │ │ ├── severity.go │ │ │ ├── severity_test.go │ │ │ ├── transformers.go │ │ │ └── writer.go │ │ ├── default_schema_version.go │ │ ├── generate.go │ │ ├── internal/ │ │ │ ├── codename/ │ │ │ │ ├── codename.go │ │ │ │ ├── codename_test.go │ │ │ │ ├── codenames_generated.go │ │ │ │ └── generate/ │ │ │ │ └── main.go │ │ │ ├── gormadapter/ │ │ │ │ ├── logger.go │ │ │ │ ├── open.go │ │ │ │ └── open_test.go │ │ │ ├── provider/ │ │ │ │ └── unmarshal/ │ │ │ │ ├── annotated_openvex_vulnerability.go │ │ │ │ ├── eol.go │ │ │ │ ├── epss.go │ │ │ │ ├── errors.go │ │ │ │ ├── github_advisory.go │ │ │ │ ├── items_envelope.go │ │ │ │ ├── known_exploited_vulnerability.go │ │ │ │ ├── match_exclusion.go │ │ │ │ ├── msrc_vulnerability.go │ │ │ │ ├── nvd/ │ │ │ │ │ ├── cve.go │ │ │ │ │ ├── cve_test.go │ │ │ │ │ ├── cvss20/ │ │ │ │ │ │ └── cvss20.go │ │ │ │ │ ├── cvss30/ │ │ │ │ │ │ └── cvss30.go │ │ │ │ │ ├── cvss31/ │ │ │ │ │ │ └── cvss31.go │ │ │ │ │ └── cvss40/ │ │ │ │ │ └── cvss40.go │ │ │ │ ├── nvd_vulnerability.go │ │ │ │ ├── openvex_vulnerability.go │ │ │ │ ├── os_vulnerability.go │ │ │ │ ├── os_vulnerability_test.go │ │ │ │ ├── osv_vulnerability.go │ │ │ │ └── single_or_multi.go │ │ │ ├── sqlite/ │ │ │ │ ├── nullable_types.go │ │ │ │ └── nullable_types_test.go │ │ │ ├── tarutil/ │ │ │ │ ├── file_entry.go │ │ │ │ ├── file_entry_test.go │ │ │ │ ├── populate.go │ │ │ │ ├── populate_test.go │ │ │ │ ├── reader_entry.go │ │ │ │ ├── reader_entry_test.go │ │ │ │ ├── tar.go │ │ │ │ ├── writer.go │ │ │ │ └── writer_test.go │ │ │ ├── testutil/ │ │ │ │ └── utils.go │ │ │ └── versionutil/ │ │ │ ├── clean_fixed_in_version.go │ │ │ ├── constraint.go │ │ │ └── constraint_test.go │ │ ├── package.go │ │ ├── package_legacy.go │ │ ├── processors/ │ │ │ ├── annotated_openvex_processor.go │ │ │ ├── eol_processor.go │ │ │ ├── eol_processor_test.go │ │ │ ├── epss_processor.go │ │ │ ├── epss_processor_test.go │ │ │ ├── github_processor.go │ │ │ ├── github_processor_test.go │ │ │ ├── kev_processor.go │ │ │ ├── kev_processor_test.go │ │ │ ├── match_exclusion_processor.go │ │ │ ├── match_exclusion_processor_test.go │ │ │ ├── msrc_processor.go │ │ │ ├── msrc_processor_test.go │ │ │ ├── nvd_processor.go │ │ │ ├── nvd_processor_test.go │ │ │ ├── openvex_processor.go │ │ │ ├── openvex_processor_test.go │ │ │ ├── os_processor.go │ │ │ ├── os_processor_test.go │ │ │ ├── osv_processor.go │ │ │ ├── osv_processor_test.go │ │ │ ├── testdata/ │ │ │ │ ├── eol-with-empty.json │ │ │ │ ├── eol.json │ │ │ │ ├── epss.json │ │ │ │ ├── exclusions.json │ │ │ │ ├── github.json │ │ │ │ ├── kev.json │ │ │ │ ├── msrc.json │ │ │ │ ├── nvd.json │ │ │ │ ├── openvex.json │ │ │ │ ├── oracle.json │ │ │ │ ├── os.json │ │ │ │ └── osv.json │ │ │ ├── version.go │ │ │ └── version_test.go │ │ ├── provider/ │ │ │ ├── entry/ │ │ │ │ ├── file.go │ │ │ │ ├── opener.go │ │ │ │ └── sqlite.go │ │ │ ├── file.go │ │ │ ├── model.go │ │ │ ├── model_test.go │ │ │ ├── provider.go │ │ │ ├── state.go │ │ │ ├── state_test.go │ │ │ └── workspace.go │ │ ├── v5/ │ │ │ ├── advisory.go │ │ │ ├── build/ │ │ │ │ ├── processors.go │ │ │ │ ├── transformers/ │ │ │ │ │ ├── entry.go │ │ │ │ │ ├── github/ │ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ │ ├── github-github-npm-0.json │ │ │ │ │ │ │ ├── github-github-python-0.json │ │ │ │ │ │ │ ├── github-github-python-1.json │ │ │ │ │ │ │ ├── github-withdrawn.json │ │ │ │ │ │ │ └── multiple-fixed-in-names.json │ │ │ │ │ │ ├── transform.go │ │ │ │ │ │ └── transform_test.go │ │ │ │ │ ├── matchexclusions/ │ │ │ │ │ │ └── transform.go │ │ │ │ │ ├── msrc/ │ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ │ └── microsoft-msrc-0.json │ │ │ │ │ │ ├── transform.go │ │ │ │ │ │ └── transform_test.go │ │ │ │ │ ├── nvd/ │ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ │ ├── CVE-2023-45283-platform-cpe-first.json │ │ │ │ │ │ │ ├── CVE-2023-45283-platform-cpe-last.json │ │ │ │ │ │ │ ├── compound-pkg.json │ │ │ │ │ │ │ ├── cve-2020-10729.json │ │ │ │ │ │ │ ├── cve-2022-0543.json │ │ │ │ │ │ │ ├── invalid_cpe.json │ │ │ │ │ │ │ ├── multiple-platforms-with-application-cpe.json │ │ │ │ │ │ │ ├── platform-cpe.json │ │ │ │ │ │ │ ├── single-package-multi-distro.json │ │ │ │ │ │ │ ├── unmarshal-test.json │ │ │ │ │ │ │ └── version-range.json │ │ │ │ │ │ ├── transform.go │ │ │ │ │ │ ├── transform_test.go │ │ │ │ │ │ ├── unique_pkg.go │ │ │ │ │ │ ├── unique_pkg_test.go │ │ │ │ │ │ └── unique_pkg_tracker.go │ │ │ │ │ ├── os/ │ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ │ ├── alpine-3.9.json │ │ │ │ │ │ │ ├── amazon-multiple-kernel-advisories.json │ │ │ │ │ │ │ ├── amzn.json │ │ │ │ │ │ │ ├── azure-linux-3.json │ │ │ │ │ │ │ ├── debian-8-multiple-entries-for-same-package.json │ │ │ │ │ │ │ ├── debian-8.json │ │ │ │ │ │ │ ├── mariner-20.json │ │ │ │ │ │ │ ├── mariner-range.json │ │ │ │ │ │ │ ├── ol-8-modules.json │ │ │ │ │ │ │ ├── ol-8.json │ │ │ │ │ │ │ ├── photon-4.0.json │ │ │ │ │ │ │ ├── rhel-8-eus.json │ │ │ │ │ │ │ ├── rhel-8-modules.json │ │ │ │ │ │ │ ├── rhel-8.json │ │ │ │ │ │ │ └── unmarshal-test.json │ │ │ │ │ │ ├── transform.go │ │ │ │ │ │ └── transform_test.go │ │ │ │ │ └── vulnerability_metadata.go │ │ │ │ ├── writer.go │ │ │ │ └── writer_test.go │ │ │ ├── cvss.go │ │ │ ├── diff.go │ │ │ ├── differ/ │ │ │ │ ├── differ.go │ │ │ │ ├── differ_test.go │ │ │ │ └── testdata/ │ │ │ │ ├── dbs/ │ │ │ │ │ ├── base/ │ │ │ │ │ │ └── 5/ │ │ │ │ │ │ └── metadata.json │ │ │ │ │ └── target/ │ │ │ │ │ └── 5/ │ │ │ │ │ └── metadata.json │ │ │ │ └── snapshot/ │ │ │ │ ├── TestPresent_Json.golden │ │ │ │ └── TestPresent_Table.golden │ │ │ ├── distribution/ │ │ │ │ ├── curator.go │ │ │ │ ├── curator_test.go │ │ │ │ ├── listing.go │ │ │ │ ├── listing_entry.go │ │ │ │ ├── listing_test.go │ │ │ │ ├── metadata.go │ │ │ │ ├── metadata_test.go │ │ │ │ ├── status.go │ │ │ │ └── testdata/ │ │ │ │ ├── curator-validate/ │ │ │ │ │ ├── bad-checksum/ │ │ │ │ │ │ └── metadata.json │ │ │ │ │ └── good-checksum/ │ │ │ │ │ └── metadata.json │ │ │ │ ├── listing-sorted.json │ │ │ │ ├── listing-unsorted.json │ │ │ │ ├── listing.json │ │ │ │ ├── metadata-edt-timezone/ │ │ │ │ │ └── metadata.json │ │ │ │ ├── metadata-gocase/ │ │ │ │ │ └── metadata.json │ │ │ │ └── tls/ │ │ │ │ ├── .gitignore │ │ │ │ ├── Makefile │ │ │ │ ├── README.md │ │ │ │ ├── generate-x509-cert-pair.sh │ │ │ │ ├── listing.py │ │ │ │ └── serve.py │ │ │ ├── fix.go │ │ │ ├── id.go │ │ │ ├── match_exclusion_provider.go │ │ │ ├── namespace/ │ │ │ │ ├── cpe/ │ │ │ │ │ ├── namespace.go │ │ │ │ │ └── namespace_test.go │ │ │ │ ├── distro/ │ │ │ │ │ ├── namespace.go │ │ │ │ │ └── namespace_test.go │ │ │ │ ├── from_string.go │ │ │ │ ├── from_string_test.go │ │ │ │ ├── language/ │ │ │ │ │ ├── namespace.go │ │ │ │ │ └── namespace_test.go │ │ │ │ └── namespace.go │ │ │ ├── pkg/ │ │ │ │ ├── qualifier/ │ │ │ │ │ ├── from_json.go │ │ │ │ │ ├── platformcpe/ │ │ │ │ │ │ └── qualifier.go │ │ │ │ │ ├── qualifier.go │ │ │ │ │ └── rpmmodularity/ │ │ │ │ │ └── qualifier.go │ │ │ │ └── resolver/ │ │ │ │ ├── java/ │ │ │ │ │ ├── resolver.go │ │ │ │ │ └── resolver_test.go │ │ │ │ ├── python/ │ │ │ │ │ ├── resolver.go │ │ │ │ │ └── resolver_test.go │ │ │ │ ├── resolver.go │ │ │ │ ├── resolver_test.go │ │ │ │ └── stock/ │ │ │ │ ├── resolver.go │ │ │ │ └── resolver_test.go │ │ │ ├── provider_store.go │ │ │ ├── schema_version.go │ │ │ ├── store/ │ │ │ │ ├── diff.go │ │ │ │ ├── diff_test.go │ │ │ │ ├── model/ │ │ │ │ │ ├── id.go │ │ │ │ │ ├── vulnerability.go │ │ │ │ │ ├── vulnerability_match_exclusion.go │ │ │ │ │ ├── vulnerability_match_exclusion_test.go │ │ │ │ │ ├── vulnerability_metadata.go │ │ │ │ │ └── vulnerability_test.go │ │ │ │ ├── store.go │ │ │ │ └── store_test.go │ │ │ ├── store.go │ │ │ ├── vulnerability.go │ │ │ ├── vulnerability_match_exclusion.go │ │ │ ├── vulnerability_match_exclusion_store.go │ │ │ ├── vulnerability_metadata.go │ │ │ ├── vulnerability_metadata_store.go │ │ │ └── vulnerability_store.go │ │ └── v6/ │ │ ├── affected_cpe_store.go │ │ ├── affected_cpe_store_test.go │ │ ├── affected_package_store.go │ │ ├── affected_package_store_test.go │ │ ├── blob_store.go │ │ ├── blob_store_test.go │ │ ├── blobs.go │ │ ├── blobs_test.go │ │ ├── build/ │ │ │ ├── archive.go │ │ │ ├── processors.go │ │ │ ├── transformers/ │ │ │ │ ├── entry.go │ │ │ │ ├── eol/ │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── epss/ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ └── go-case.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── github/ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ ├── GHSA-2wgc-48g2-cj5w.json │ │ │ │ │ │ ├── GHSA-3x74-v64j-qc3f.json │ │ │ │ │ │ ├── GHSA-92cp-5422-2mw7.json │ │ │ │ │ │ ├── GHSA-qc55-vm3j-74gp.json │ │ │ │ │ │ ├── github-github-npm-0.json │ │ │ │ │ │ ├── github-github-python-0.json │ │ │ │ │ │ ├── github-withdrawn.json │ │ │ │ │ │ └── multiple-fixed-in-names.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── internal/ │ │ │ │ │ ├── sort.go │ │ │ │ │ ├── time.go │ │ │ │ │ └── time_test.go │ │ │ │ ├── kev/ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ └── go-case.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── msrc/ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ └── microsoft-msrc-0.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── nvd/ │ │ │ │ │ ├── affected_range.go │ │ │ │ │ ├── affected_range_test.go │ │ │ │ │ ├── node.go │ │ │ │ │ ├── node_test.go │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ ├── CVE-2004-0377.json │ │ │ │ │ │ ├── CVE-2008-3442.json │ │ │ │ │ │ ├── CVE-2023-45283-platform-cpe-first.json │ │ │ │ │ │ ├── CVE-2023-45283-platform-cpe-last.json │ │ │ │ │ │ ├── compound-pkg.json │ │ │ │ │ │ ├── cve-2020-10729.json │ │ │ │ │ │ ├── cve-2021-1566.json │ │ │ │ │ │ ├── cve-2022-0543.json │ │ │ │ │ │ ├── cve-2024-26663-standalone-os.json │ │ │ │ │ │ ├── fix-version.json │ │ │ │ │ │ ├── fix-wrong-version.json │ │ │ │ │ │ ├── invalid_cpe.json │ │ │ │ │ │ ├── jvm-packages.json │ │ │ │ │ │ ├── multiple-platforms-with-application-cpe.json │ │ │ │ │ │ ├── platform-cpe.json │ │ │ │ │ │ ├── single-package-multi-distro.json │ │ │ │ │ │ └── version-range.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── openvex/ │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── os/ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ ├── alpine-3.9.json │ │ │ │ │ │ ├── amazon-multiple-kernel-advisories.json │ │ │ │ │ │ ├── amzn.json │ │ │ │ │ │ ├── azure-linux-3.json │ │ │ │ │ │ ├── debian-8-multiple-entries-for-same-package.json │ │ │ │ │ │ ├── debian-8.json │ │ │ │ │ │ ├── fedora-39.json │ │ │ │ │ │ ├── mariner-20.json │ │ │ │ │ │ ├── mariner-range.json │ │ │ │ │ │ ├── ol-8-modules.json │ │ │ │ │ │ ├── ol-8.json │ │ │ │ │ │ ├── rhel-8-modules.json │ │ │ │ │ │ └── rhel-8.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ ├── osv/ │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ ├── ALSA-2025-7467.json │ │ │ │ │ │ ├── BIT-apache-2020-11984.json │ │ │ │ │ │ └── BIT-node-2020-8201.json │ │ │ │ │ ├── transform.go │ │ │ │ │ └── transform_test.go │ │ │ │ └── references.go │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ ├── cache.go │ │ ├── cache_test.go │ │ ├── cpe_store.go │ │ ├── data.go │ │ ├── db.go │ │ ├── db_metadata_store.go │ │ ├── db_metadata_store_test.go │ │ ├── description.go │ │ ├── description_test.go │ │ ├── distribution/ │ │ │ ├── client.go │ │ │ ├── client_test.go │ │ │ ├── latest.go │ │ │ ├── latest_test.go │ │ │ └── status.go │ │ ├── enumerations.go │ │ ├── enumerations_test.go │ │ ├── fillers.go │ │ ├── import_metadata.go │ │ ├── import_metadata_test.go │ │ ├── installation/ │ │ │ ├── curator.go │ │ │ └── curator_test.go │ │ ├── log_dropped.go │ │ ├── models.go │ │ ├── models_test.go │ │ ├── name/ │ │ │ ├── java.go │ │ │ ├── java_test.go │ │ │ ├── python.go │ │ │ ├── python_test.go │ │ │ └── resolver.go │ │ ├── operating_system_store.go │ │ ├── operating_system_store_test.go │ │ ├── package_store.go │ │ ├── provider_store.go │ │ ├── provider_store_test.go │ │ ├── refs.go │ │ ├── schema/ │ │ │ └── main.go │ │ ├── search_query.go │ │ ├── search_query_test.go │ │ ├── severity.go │ │ ├── severity_test.go │ │ ├── store.go │ │ ├── store_test.go │ │ ├── unaffected_cpe_store.go │ │ ├── unaffected_cpe_store_test.go │ │ ├── unaffected_package_store.go │ │ ├── unaffected_package_store_test.go │ │ ├── vulnerability.go │ │ ├── vulnerability_decorator_store.go │ │ ├── vulnerability_decorator_store_test.go │ │ ├── vulnerability_provider.go │ │ ├── vulnerability_provider_mocks_test.go │ │ ├── vulnerability_provider_test.go │ │ ├── vulnerability_store.go │ │ ├── vulnerability_store_test.go │ │ └── vulnerability_test.go │ ├── deprecated.go │ ├── distro/ │ │ ├── distro.go │ │ ├── distro_test.go │ │ ├── fix_channel.go │ │ ├── fix_channel_test.go │ │ ├── testdata/ │ │ │ ├── bad-id │ │ │ ├── bad-redhat-release │ │ │ ├── bad-system-release-cpe │ │ │ ├── centos-8 │ │ │ ├── debian-8 │ │ │ ├── os/ │ │ │ │ ├── almalinux/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── alpine/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── alpine-edge/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── amazon/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── arch/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── azurelinux/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── busybox/ │ │ │ │ │ └── bin/ │ │ │ │ │ └── busybox │ │ │ │ ├── centos/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ ├── centos5/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── redhat-release │ │ │ │ ├── centos6/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── system-release-cpe │ │ │ │ ├── chainguard/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── custom/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── debian/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ ├── debian-sid/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ ├── echo/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── empty/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── fedora/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ ├── gentoo/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── mariner/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── minimos/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── opensuse-leap/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── oraclelinux/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── photon/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── postmarketos/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── postmarketos-edge/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── raspbian/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── redhat/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ ├── rockylinux/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── scientific/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── scientific6/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── redhat-release │ │ │ │ ├── secureos/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── sles/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ ├── ubuntu/ │ │ │ │ │ └── etc/ │ │ │ │ │ └── os-release │ │ │ │ └── wolfi/ │ │ │ │ └── etc/ │ │ │ │ └── os-release │ │ │ ├── partial-fields/ │ │ │ │ ├── missing-id/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ ├── missing-version/ │ │ │ │ │ └── usr/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── os-release │ │ │ │ └── unknown-id/ │ │ │ │ └── usr/ │ │ │ │ └── lib/ │ │ │ │ └── os-release │ │ │ ├── rhel-8 │ │ │ ├── ubuntu-20.04 │ │ │ └── unprintable │ │ ├── type.go │ │ └── type_test.go │ ├── event/ │ │ ├── event.go │ │ ├── monitor/ │ │ │ ├── db_diff.go │ │ │ └── matching.go │ │ └── parsers/ │ │ └── parsers.go │ ├── grypeerr/ │ │ ├── errors.go │ │ └── expected_error.go │ ├── internal/ │ │ ├── generate.go │ │ └── packagemetadata/ │ │ ├── discover_type_names.go │ │ ├── generate/ │ │ │ └── main.go │ │ ├── generated.go │ │ ├── names.go │ │ └── names_test.go │ ├── lib.go │ ├── load_vulnerability_db.go │ ├── load_vulnerability_db_bench_test.go │ ├── match/ │ │ ├── details.go │ │ ├── details_test.go │ │ ├── explicit_ignores.go │ │ ├── explicit_ignores_test.go │ │ ├── fingerprint.go │ │ ├── ignore.go │ │ ├── ignore_test.go │ │ ├── match.go │ │ ├── match_test.go │ │ ├── matcher.go │ │ ├── matcher_type.go │ │ ├── matches.go │ │ ├── matches_test.go │ │ ├── provider.go │ │ ├── results.go │ │ ├── sort.go │ │ └── type.go │ ├── matcher/ │ │ ├── apk/ │ │ │ ├── matcher.go │ │ │ └── matcher_test.go │ │ ├── bitnami/ │ │ │ └── matcher.go │ │ ├── dotnet/ │ │ │ └── matcher.go │ │ ├── dpkg/ │ │ │ ├── matcher.go │ │ │ ├── matcher_mocks_test.go │ │ │ └── matcher_test.go │ │ ├── golang/ │ │ │ ├── matcher.go │ │ │ └── matcher_test.go │ │ ├── hex/ │ │ │ └── matcher.go │ │ ├── internal/ │ │ │ ├── common.go │ │ │ ├── cpe.go │ │ │ ├── cpe_test.go │ │ │ ├── distro.go │ │ │ ├── distro_test.go │ │ │ ├── eol.go │ │ │ ├── eol_test.go │ │ │ ├── language.go │ │ │ ├── language_test.go │ │ │ ├── only_non_withdrawn_vulnerabilities.go │ │ │ ├── only_qualified_packages.go │ │ │ ├── only_vulnerable_targets.go │ │ │ ├── only_vulnerable_targets_test.go │ │ │ ├── only_vulnerable_versions.go │ │ │ ├── result/ │ │ │ │ ├── match_details_set.go │ │ │ │ ├── provider.go │ │ │ │ ├── results.go │ │ │ │ └── results_test.go │ │ │ └── utils_test.go │ │ ├── java/ │ │ │ ├── matcher.go │ │ │ ├── matcher_integration_test.go │ │ │ ├── matcher_mocks_test.go │ │ │ ├── matcher_test.go │ │ │ ├── maven_search.go │ │ │ └── maven_test.go │ │ ├── javascript/ │ │ │ └── matcher.go │ │ ├── matchers.go │ │ ├── mock/ │ │ │ └── matcher.go │ │ ├── msrc/ │ │ │ └── matcher.go │ │ ├── pacman/ │ │ │ ├── matcher.go │ │ │ └── matcher_test.go │ │ ├── portage/ │ │ │ ├── matcher.go │ │ │ ├── matcher_mocks_test.go │ │ │ └── matcher_test.go │ │ ├── python/ │ │ │ └── matcher.go │ │ ├── rpm/ │ │ │ ├── almalinux.go │ │ │ ├── almalinux_package_utils.go │ │ │ ├── almalinux_package_utils_test.go │ │ │ ├── almalinux_test.go │ │ │ ├── matcher.go │ │ │ ├── matcher_mocks_test.go │ │ │ ├── matcher_test.go │ │ │ ├── rhel_eus.go │ │ │ └── rhel_eus_test.go │ │ ├── ruby/ │ │ │ └── matcher.go │ │ ├── rust/ │ │ │ └── matcher.go │ │ └── stock/ │ │ ├── matcher.go │ │ └── matcher_test.go │ ├── pkg/ │ │ ├── apk_metadata.go │ │ ├── context.go │ │ ├── context_test.go │ │ ├── cpe_provider.go │ │ ├── cpe_provider_test.go │ │ ├── file_owner.go │ │ ├── golang_metadata.go │ │ ├── java_metadata.go │ │ ├── java_metadata_test.go │ │ ├── package.go │ │ ├── package_test.go │ │ ├── provider.go │ │ ├── provider_config.go │ │ ├── provider_test.go │ │ ├── purl_provider.go │ │ ├── purl_provider_test.go │ │ ├── qualifier/ │ │ │ ├── platformcpe/ │ │ │ │ ├── qualifier.go │ │ │ │ └── qualifier_test.go │ │ │ ├── qualifier.go │ │ │ └── rpmmodularity/ │ │ │ ├── qualifier.go │ │ │ └── qualifier_test.go │ │ ├── rpm_metadata.go │ │ ├── syft_provider.go │ │ ├── syft_sbom_provider.go │ │ ├── syft_sbom_provider_test.go │ │ ├── testdata/ │ │ │ ├── alpine-tampered.att.json │ │ │ ├── alpine-tampered.cdx.att.json │ │ │ ├── alpine.att.json │ │ │ ├── alpine.cdx.att.json │ │ │ ├── another_cosign.pub │ │ │ ├── bad-sbom.json │ │ │ ├── cosign.pub │ │ │ ├── cosign_broken.pub │ │ │ ├── image-simple/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── package.json │ │ │ │ └── target/ │ │ │ │ └── nested/ │ │ │ │ └── package.json │ │ │ ├── invalid.json │ │ │ ├── purl/ │ │ │ │ ├── different-os.txt │ │ │ │ ├── empty.json │ │ │ │ ├── homogeneous-os.txt │ │ │ │ ├── invalid-cpe.txt │ │ │ │ ├── invalid-purl.txt │ │ │ │ ├── valid-purl.txt │ │ │ │ ├── valid-rhel-9+eus.txt │ │ │ │ └── valid-rhel-9.txt │ │ │ ├── sbom-with-intoto-string.json │ │ │ ├── syft-java-bad-cpes.json │ │ │ ├── syft-multiple-ecosystems.json │ │ │ └── syft-spring.json │ │ ├── upstream_package.go │ │ ├── upstream_package_test.go │ │ ├── version_format.go │ │ └── version_format_test.go │ ├── presenter/ │ │ ├── cyclonedx/ │ │ │ ├── presenter.go │ │ │ ├── presenter_test.go │ │ │ ├── testdata/ │ │ │ │ └── snapshot/ │ │ │ │ ├── TestCycloneDxPresenterDir.golden │ │ │ │ └── TestCycloneDxPresenterImage.golden │ │ │ ├── vulnerability.go │ │ │ └── vulnerability_test.go │ │ ├── explain/ │ │ │ ├── __snapshots__/ │ │ │ │ └── explain_snapshot_test.snap │ │ │ ├── explain.go │ │ │ ├── explain_cve.tmpl │ │ │ ├── explain_snapshot_test.go │ │ │ └── testdata/ │ │ │ ├── chainguard-ruby-test.json │ │ │ ├── ghsa-test.json │ │ │ └── keycloak-test.json │ │ ├── internal/ │ │ │ └── test_helpers.go │ │ ├── json/ │ │ │ ├── presenter.go │ │ │ ├── presenter_test.go │ │ │ └── testdata/ │ │ │ ├── image-simple/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── file-1.txt │ │ │ │ ├── file-2.txt │ │ │ │ └── target/ │ │ │ │ └── really/ │ │ │ │ └── nested/ │ │ │ │ └── file-3.txt │ │ │ └── snapshot/ │ │ │ ├── TestEmptyJsonPresenter.golden │ │ │ ├── TestJsonDirsPresenter.golden │ │ │ ├── TestJsonImgsPresenter.golden │ │ │ ├── anchore-fixture-image-simple.golden │ │ │ └── stereoscope-fixture-image-simple.golden │ │ ├── models/ │ │ │ ├── alert.go │ │ │ ├── alert_test.go │ │ │ ├── cvss.go │ │ │ ├── descriptor.go │ │ │ ├── distribution.go │ │ │ ├── document.go │ │ │ ├── document_test.go │ │ │ ├── ignore.go │ │ │ ├── ignore_test.go │ │ │ ├── match.go │ │ │ ├── metadata_mock.go │ │ │ ├── package.go │ │ │ ├── presenter_bundle.go │ │ │ ├── sort.go │ │ │ ├── sort_test.go │ │ │ ├── source.go │ │ │ ├── source_test.go │ │ │ ├── vulnerability.go │ │ │ ├── vulnerability_metadata.go │ │ │ └── vulnerability_test.go │ │ ├── presenter.go │ │ ├── sarif/ │ │ │ ├── presenter.go │ │ │ ├── presenter_test.go │ │ │ └── testdata/ │ │ │ ├── image-simple/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── file-1.txt │ │ │ │ ├── file-2.txt │ │ │ │ └── target/ │ │ │ │ └── really/ │ │ │ │ └── nested/ │ │ │ │ └── file-3.txt │ │ │ └── snapshot/ │ │ │ ├── TestSarifPresenter_directory.golden │ │ │ └── TestSarifPresenter_image.golden │ │ ├── table/ │ │ │ ├── __snapshots__/ │ │ │ │ └── presenter_test.snap │ │ │ ├── presenter.go │ │ │ ├── presenter_test.go │ │ │ └── testdata/ │ │ │ └── snapshot/ │ │ │ └── TestTablePresenter_Color.golden │ │ └── template/ │ │ ├── presenter.go │ │ ├── presenter_test.go │ │ └── testdata/ │ │ ├── snapshot/ │ │ │ └── TestPresenter_Present.golden │ │ ├── test.template │ │ ├── test.template.sprig.date │ │ └── test.valid.template │ ├── search/ │ │ ├── cpe.go │ │ ├── cpe_test.go │ │ ├── criteria.go │ │ ├── criteria_test.go │ │ ├── distro.go │ │ ├── distro_test.go │ │ ├── ecosystem.go │ │ ├── ecosystem_test.go │ │ ├── func.go │ │ ├── func_test.go │ │ ├── id.go │ │ ├── id_test.go │ │ ├── package_name.go │ │ ├── package_name_test.go │ │ ├── unaffected.go │ │ ├── version_constraint.go │ │ └── version_constraint_test.go │ ├── version/ │ │ ├── apk_version.go │ │ ├── apk_version_test.go │ │ ├── bitnami_version.go │ │ ├── bitnami_version_test.go │ │ ├── combined_constraint.go │ │ ├── combined_constraint_test.go │ │ ├── comparator.go │ │ ├── constraint.go │ │ ├── deb_version.go │ │ ├── deb_version_test.go │ │ ├── deprecated.go │ │ ├── error.go │ │ ├── format.go │ │ ├── format_test.go │ │ ├── fuzzy_constraint.go │ │ ├── fuzzy_constraint_test.go │ │ ├── fuzzy_version.go │ │ ├── fuzzy_version_test.go │ │ ├── gem_version.go │ │ ├── gem_version_test.go │ │ ├── generic_constraint.go │ │ ├── generic_constraint_test.go │ │ ├── golang_version.go │ │ ├── golang_version_test.go │ │ ├── helper_test.go │ │ ├── jvm_version.go │ │ ├── jvm_version_test.go │ │ ├── kb_constraint.go │ │ ├── kb_constraint_test.go │ │ ├── kb_version.go │ │ ├── kb_version_test.go │ │ ├── maven_version.go │ │ ├── maven_version_test.go │ │ ├── operator.go │ │ ├── pacman_version.go │ │ ├── pacman_version_test.go │ │ ├── pep440_version.go │ │ ├── pep440_version_test.go │ │ ├── portage_version.go │ │ ├── portage_version_test.go │ │ ├── range.go │ │ ├── range_expression.go │ │ ├── range_expression_test.go │ │ ├── range_test.go │ │ ├── rpm_version.go │ │ ├── rpm_version_test.go │ │ ├── semantic_version.go │ │ ├── semantic_version_test.go │ │ ├── set.go │ │ ├── set_test.go │ │ ├── version.go │ │ └── version_test.go │ ├── vex/ │ │ ├── csaf/ │ │ │ ├── csaf.go │ │ │ ├── csaf_test.go │ │ │ ├── implementation.go │ │ │ ├── implementation_test.go │ │ │ └── status.go │ │ ├── openvex/ │ │ │ ├── implementation.go │ │ │ └── implementation_test.go │ │ ├── processor.go │ │ ├── processor_test.go │ │ ├── status/ │ │ │ └── status.go │ │ └── testdata/ │ │ └── vex-docs/ │ │ ├── csaf-demo1.json │ │ ├── csaf-demo2.json │ │ ├── openvex-debian.json │ │ ├── openvex-demo1.json │ │ ├── openvex-demo2.json │ │ ├── openvex-image-no-subcomponents.json │ │ └── openvex-package-product.json │ ├── vulnerability/ │ │ ├── advisory.go │ │ ├── fix.go │ │ ├── metadata.go │ │ ├── metadata_test.go │ │ ├── mock/ │ │ │ └── vulnerability_provider.go │ │ ├── provider.go │ │ ├── severity.go │ │ └── vulnerability.go │ ├── vulnerability_matcher.go │ └── vulnerability_matcher_test.go ├── install.sh ├── internal/ │ ├── bus/ │ │ ├── bus.go │ │ └── helpers.go │ ├── cvss/ │ │ ├── metrics.go │ │ └── metrics_test.go │ ├── dbtest/ │ │ ├── default_vulnerabilities.go │ │ ├── server.go │ │ └── server_test.go │ ├── file/ │ │ ├── copy.go │ │ ├── exists.go │ │ ├── getter.go │ │ ├── getter_test.go │ │ ├── hasher.go │ │ ├── hasher_test.go │ │ ├── tar_xz_decompressor.go │ │ ├── tar_xz_decompressor_test.go │ │ ├── xz_decompressor.go │ │ └── xz_decompressor_test.go │ ├── format/ │ │ ├── format.go │ │ ├── format_test.go │ │ ├── presenter.go │ │ ├── writer.go │ │ └── writer_test.go │ ├── input.go │ ├── log/ │ │ ├── errors.go │ │ └── log.go │ ├── redact/ │ │ └── redact.go │ ├── regex_helpers.go │ ├── regex_helpers_test.go │ ├── schemaver/ │ │ ├── schema_ver.go │ │ └── schema_ver_test.go │ ├── stringutil/ │ │ ├── color.go │ │ ├── parse.go │ │ ├── string_helpers.go │ │ ├── string_helpers_test.go │ │ ├── stringset.go │ │ └── tprint.go │ └── testutils/ │ └── golden_file.go ├── llms.txt ├── schema/ │ └── grype/ │ ├── db/ │ │ ├── README.md │ │ ├── blob/ │ │ │ └── json/ │ │ │ ├── schema-6.1.1.json │ │ │ ├── schema-6.1.2.json │ │ │ ├── schema-6.1.3.json │ │ │ ├── schema-6.1.4.json │ │ │ └── schema-latest.json │ │ └── sql/ │ │ ├── schema-6.1.1.sql │ │ ├── schema-6.1.2.sql │ │ ├── schema-6.1.3.sql │ │ ├── schema-6.1.4.sql │ │ └── schema-latest.sql │ ├── db-search/ │ │ └── json/ │ │ ├── README.md │ │ ├── schema-1.0.0.json │ │ ├── schema-1.0.1.json │ │ ├── schema-1.0.2.json │ │ ├── schema-1.0.3.json │ │ ├── schema-1.1.0.json │ │ ├── schema-1.1.1.json │ │ ├── schema-1.1.2.json │ │ ├── schema-1.1.3.json │ │ └── schema-latest.json │ └── db-search-vuln/ │ └── json/ │ ├── README.md │ ├── schema-1.0.0.json │ ├── schema-1.0.1.json │ ├── schema-1.0.3.json │ ├── schema-1.0.4.json │ ├── schema-1.0.5.json │ └── schema-latest.json ├── templates/ │ ├── README.md │ ├── csv.tmpl │ ├── html.tmpl │ ├── junit.tmpl │ ├── markdown.tmpl │ └── table.tmpl └── test/ ├── cli/ │ ├── cmd_test.go │ ├── config_test.go │ ├── db_providers_test.go │ ├── db_validations_test.go │ ├── registry_auth_test.go │ ├── sbom_input_test.go │ ├── subprocess_test.go │ ├── testdata/ │ │ ├── Makefile │ │ ├── another_cosign.pub │ │ ├── configs/ │ │ │ ├── dir1/ │ │ │ │ └── .grype.yaml │ │ │ ├── dir2/ │ │ │ │ └── .grype.yaml │ │ │ └── dir3/ │ │ │ └── .grype.yaml │ │ ├── cosign.pub │ │ ├── cosign_broken.pub │ │ ├── empty.json │ │ ├── image-bare/ │ │ │ ├── Dockerfile │ │ │ └── file-1.txt │ │ ├── image-java-subprocess/ │ │ │ ├── Dockerfile │ │ │ └── app.java │ │ ├── image-node-subprocess/ │ │ │ ├── Dockerfile │ │ │ └── app.js │ │ ├── sbom-grype-source.json │ │ ├── sbom-ubuntu-20.04--pruned.json │ │ └── test-ignore-reason/ │ │ ├── config-with-ignore.yaml │ │ ├── sbom.json │ │ └── template-with-ignore-reasons │ ├── trait_assertions_test.go │ ├── utils_test.go │ └── version_cmd_test.go ├── grype-test-config.yaml ├── ignore-att-signature.yaml ├── install/ │ ├── .dockerignore │ ├── .gitignore │ ├── 0_checksums_test.sh │ ├── 1_download_snapshot_asset_test.sh │ ├── 2_download_release_asset_test.sh │ ├── 3_install_asset_test.sh │ ├── 4_prep_signature_verification_test.sh │ ├── Makefile │ ├── environments/ │ │ ├── Dockerfile-alpine-3.6 │ │ ├── Dockerfile-busybox-1.36 │ │ └── Dockerfile-ubuntu-20.04 │ ├── github_test.sh │ ├── test_harness.sh │ └── testdata/ │ ├── assets/ │ │ ├── invalid/ │ │ │ ├── .gitignore │ │ │ └── checksums.txt │ │ └── valid/ │ │ ├── .gitignore │ │ └── checksums.txt │ ├── github-api-grype-v0.32.0-release.json │ ├── grype_0.32.0-SNAPSHOT-d461f63_checksums.txt │ └── grype_0.32.0_checksums.txt ├── integration/ │ ├── compare_sbom_input_vs_lib_test.go │ ├── db_mock_test.go │ ├── match_by_image_test.go │ ├── match_by_sbom_document_test.go │ ├── testdata/ │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── image-alpine-match-coverage/ │ │ │ ├── Dockerfile │ │ │ ├── etc/ │ │ │ │ └── os-release │ │ │ └── lib/ │ │ │ └── apk/ │ │ │ └── db/ │ │ │ └── installed │ │ ├── image-arch-match-coverage/ │ │ │ └── Dockerfile │ │ ├── image-centos-match-coverage/ │ │ │ ├── Dockerfile │ │ │ ├── etc/ │ │ │ │ └── os-release │ │ │ └── var/ │ │ │ └── lib/ │ │ │ └── rpm/ │ │ │ ├── Packages │ │ │ └── generate-fixture.sh │ │ ├── image-debian-match-coverage/ │ │ │ ├── Dockerfile │ │ │ ├── dotnet/ │ │ │ │ └── TestLibrary.deps.json │ │ │ ├── golang/ │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── main.go │ │ │ ├── haskell/ │ │ │ │ ├── cabal.project.freeze │ │ │ │ └── stack.yaml │ │ │ ├── java/ │ │ │ │ └── generate-fixtures.md │ │ │ ├── javascript/ │ │ │ │ └── pkg-json/ │ │ │ │ └── package.json │ │ │ ├── python/ │ │ │ │ └── dist-info/ │ │ │ │ ├── METADATA │ │ │ │ └── top_level.txt │ │ │ ├── ruby/ │ │ │ │ └── specifications/ │ │ │ │ └── bundler.gemspec │ │ │ ├── usr/ │ │ │ │ └── lib/ │ │ │ │ └── os-release │ │ │ └── var/ │ │ │ └── lib/ │ │ │ └── dpkg/ │ │ │ └── status │ │ ├── image-jvm-match-coverage/ │ │ │ ├── Dockerfile │ │ │ └── opt/ │ │ │ └── java/ │ │ │ └── openjdk/ │ │ │ └── release │ │ ├── image-portage-match-coverage/ │ │ │ ├── Dockerfile │ │ │ ├── etc/ │ │ │ │ └── os-release │ │ │ └── var/ │ │ │ └── db/ │ │ │ ├── pkg/ │ │ │ │ └── app-containers/ │ │ │ │ └── skopeo-1.5.1/ │ │ │ │ ├── CONTENTS │ │ │ │ ├── LICENSE │ │ │ │ └── SIZE │ │ │ └── repos/ │ │ │ └── gentoo/ │ │ │ └── skel.ebuild │ │ ├── image-rust-auditable-match-coverage/ │ │ │ └── Dockerfile │ │ ├── image-sles-match-coverage/ │ │ │ ├── Dockerfile │ │ │ ├── etc/ │ │ │ │ └── os-release │ │ │ └── var/ │ │ │ └── lib/ │ │ │ └── rpm/ │ │ │ ├── Packages │ │ │ └── generate-fixture.sh │ │ ├── sbom/ │ │ │ ├── syft-sbom-with-kb-packages.json │ │ │ └── syft-sbom-with-unknown-packages.json │ │ ├── skopeo-policy.json │ │ ├── snapshot/ │ │ │ └── TestDatabaseDiff.golden │ │ └── vex/ │ │ ├── csaf/ │ │ │ ├── affected.csaf.json │ │ │ └── under_investigation.csaf.json │ │ └── openvex/ │ │ ├── affected.openvex.json │ │ └── under_investigation.openvex.json │ └── utils_test.go ├── quality/ │ ├── .gitignore │ ├── .grype.yaml │ ├── .python-version │ ├── .yardstick.yaml │ ├── Makefile │ ├── README.md │ ├── requirements.txt │ └── test-db └── validate-grype-db-schema.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .binny.yaml ================================================ tools: # we want to use a pinned version of binny to manage the toolchain (so binny manages itself!) - name: binny version: want: v0.12.0 method: github-release with: repo: anchore/binny # used to produce SBOMs during release - name: syft version: want: latest method: github-release with: repo: anchore/syft # used to sign mac binaries at release - name: quill version: want: v0.7.1 method: github-release with: repo: anchore/quill # used for linting - name: golangci-lint version: want: v2.11.3 method: github-release with: repo: golangci/golangci-lint # used for showing the changelog at release - name: glow version: want: v2.1.1 method: github-release with: repo: charmbracelet/glow # used for signing the checksums file at release - name: cosign version: want: v3.0.5 method: github-release with: repo: sigstore/cosign # used to release all artifacts - name: goreleaser version: want: v2.14.3 method: github-release with: repo: goreleaser/goreleaser # used for organizing imports during static analysis - name: gosimports version: want: v0.3.8 method: github-release with: repo: rinchsan/gosimports # used at release to generate the changelog - name: chronicle version: want: v0.8.0 method: github-release with: repo: anchore/chronicle # used during static analysis for license compliance - name: bouncer version: want: v0.4.0 method: github-release with: repo: wagoodman/go-bouncer # used for running all local and CI tasks - name: task version: want: v3.49.1 method: github-release with: repo: go-task/task # used for triggering a release - name: gh version: want: v2.88.1 method: github-release with: repo: cli/cli # used for integration tests - name: skopeo version: want: v1.22.0 method: go-install with: module: github.com/containers/skopeo entrypoint: cmd/skopeo args: - "-tags" - containers_image_openpgp env: - CGO_ENABLED=0 - GO_DYN_FLAGS="" ================================================ FILE: .bouncer.yaml ================================================ permit: - BSD.* - CC0.* - MIT.* - Apache.* - MPL.* - ISC - WTFPL ignore-packages: # packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE - github.com/anchore/packageurl-go # github.com/gocsaf/csaf is released under the Apache License, version 2.0 (Apache-2.0) # https://github.com/gocsaf/csaf/blob/main/LICENSE-Apache-2.0.txt - github.com/gocsaf/csaf/v3/csaf - github.com/gocsaf/csaf/v3/internal/misc - github.com/gocsaf/csaf/v3/util # tools-golang is released under the Apache License, version 2.0 (Apache-2.0) # https://github.com/spdx/tools-golang/blob/main/LICENSE.code - github.com/spdx/tools-golang # crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Libary - crypto/internal/boring # from: https://github.com/xi2/xz/blob/master/LICENSE # All these files have been put into the public domain. # You can do whatever you want with these files. - github.com/xi2/xz # from: https://github.com/owenrumney/go-sarif/blob/main/LICENSE # This is released into the public domain using the "Unlicense license" - github.com/owenrumney/go-sarif # github.com/sorairolake/lzip-go is released under the Apache License, version 2.0 (Apache-2.0) # https://github.com/sorairolake/lzip-go/blob/develop/LICENSE-APACHE - github.com/sorairolake/lzip-go # from: https://gitlab.com/cznic/sqlite/-/blob/v1.66.3/LICENSE # This is a BSD-3-Clause license - modernc.org/libc - modernc.org/libc/errno - modernc.org/libc/fcntl - modernc.org/libc/fts - modernc.org/libc/grp - modernc.org/libc/langinfo - modernc.org/libc/limits - modernc.org/libc/netdb - modernc.org/libc/netinet/in - modernc.org/libc/poll - modernc.org/libc/pthread - modernc.org/libc/pwd - modernc.org/libc/signal - modernc.org/libc/stdio - modernc.org/libc/stdlib - modernc.org/libc/sys/socket - modernc.org/libc/sys/stat - modernc.org/libc/sys/types - modernc.org/libc/termios - modernc.org/libc/time - modernc.org/libc/unistd - modernc.org/libc/utime - modernc.org/libc/uuid/uuid - modernc.org/libc/wctype - modernc.org/mathutil - modernc.org/memory ================================================ FILE: .chronicle.yaml ================================================ enforce-v0: true # don't make breaking-change label bump major version before 1.0. title: "" ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **What happened**: **What you expected to happen**: **How to reproduce it (as minimally and precisely as possible)**: **Anything else we need to know?**: **Environment**: - Output of `grype version`: - OS (e.g: `cat /etc/os-release` or similar): ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Join our Discourse community 💬 # link to our community Discourse site url: https://anchore.com/discourse about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!' ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **What would you like to be added**: **Why is this needed**: **Additional context**: ================================================ FILE: .github/ISSUE_TEMPLATE/match_issue.md ================================================ --- name: Vulnerability Match Issue about: Report an issue with vulnerability matching title: '' labels: [ bug, false-positive ] assignees: '' --- **Vulnerability ID**: **Package URL or steps to reproduce**: **Anything else we need to know?**: **Environment**: - Output of `grype version`: - OS (e.g: `cat /etc/os-release` or similar): ================================================ FILE: .github/actions/bootstrap/action.yaml ================================================ name: "Bootstrap" description: "Bootstrap all tools and dependencies" inputs: go-version: description: "Go version to install" required: true default: "1.25.x" python-version: description: "Python version to install" required: true default: "3.11" go-dependencies: description: "Download go dependencies" required: true default: "true" cache-key-prefix: description: "Prefix all cache keys with this value" required: true default: "1ac8281053" compute-fingerprints: description: "Compute test fixture fingerprints" required: true default: "true" tools: description: "whether to install tools" default: "true" bootstrap-apt-packages: description: "Space delimited list of tools to install via apt" default: "libxml2-utils" runs: using: "composite" steps: # note: go mod and build is automatically cached on default with v4+ - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 if: inputs.go-version != '' with: go-version: ${{ inputs.go-version }} check-latest: true - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ inputs.python-version }} - name: Restore tool cache id: tool-cache uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 if: inputs.tools == 'true' with: path: ${{ github.workspace }}/.tool key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }} - name: Install project tools if: inputs.tools == 'true' shell: bash run: | make tools .tool/binny list .tool/binny check - name: Install go dependencies if: inputs.go-dependencies == 'true' shell: bash run: make ci-bootstrap-go - name: Install apt packages if: inputs.bootstrap-apt-packages != '' shell: bash run: | read -ra PACKAGES <<< "$BOOTSTRAP_APT_PACKAGES" DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${PACKAGES[@]}" env: BOOTSTRAP_APT_PACKAGES: ${{ inputs.bootstrap-apt-packages }} - name: Create all cache fingerprints if: inputs.compute-fingerprints == 'true' shell: bash run: make fingerprints ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily cooldown: default-days: 7 - package-ecosystem: "github-actions" directory: "/.github/actions/bootstrap" schedule: interval: "daily" open-pull-requests-limit: 10 labels: - "dependencies" cooldown: default-days: 7 - package-ecosystem: "gomod" directory: "/" schedule: interval: daily cooldown: default-days: 7 ================================================ FILE: .github/scripts/check-syft-version-is-release.sh ================================================ #!/usr/bin/env bash set -e version=$(grep -E "github.com/anchore/syft" go.mod | awk '{print $NF}') # ensure that the version is a release version (not a commit hash) # a release in this case means that the go tooling resolved the version to a tag # this does not guarantee that the tag has a github release associated with it if [[ ! $version =~ ^v[0-9]+\.[0-9]+\.[0-9]?$ ]]; then echo "syft version in go.mod is not a release version: $version" echo "please update the version in go.mod to a release version and try again" exit 1 else echo "syft version in go.mod is a release version: $version" fi ================================================ FILE: .github/scripts/ci-check.sh ================================================ #!/usr/bin/env bash red=$(tput setaf 1) bold=$(tput bold) normal=$(tput sgr0) # assert we are running in CI (or die!) if [[ -z "$CI" ]]; then echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}" exit 1 fi ================================================ FILE: .github/scripts/coverage.py ================================================ #!/usr/bin/env python3 import subprocess import sys import shlex class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKCYAN = '\033[96m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' if len(sys.argv) < 3: print("Usage: coverage.py [threshold] [go-coverage-report]") sys.exit(1) threshold = float(sys.argv[1]) report = sys.argv[2] args = shlex.split(f"go tool cover -func {report}") p = subprocess.run(args, capture_output=True, text=True) percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", "")) print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}") if percent_coverage < threshold: print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}") sys.exit(1) ================================================ FILE: .github/scripts/db-schema-drift-check.sh ================================================ #!/usr/bin/env bash set -u if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then echo " 🔴 there are uncommitted changes, please commit them before running this check" exit 1 fi if ! make generate-db-schema; then echo "Generating database blob schemas failed" exit 1 fi if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then echo " 🔴 database blob schemas have uncommitted changes" echo " Run 'task generate-db-schema' and commit the changes" echo "" git status --porcelain echo "" git diff schema/grype/db/ exit 1 fi echo "✅ Database blob schemas are up to date" ================================================ FILE: .github/scripts/go-mod-tidy-check.sh ================================================ #!/usr/bin/env bash set -eu ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX") TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX") trap "cp -p ${ORIGINAL_STATE_DIR}/* ./ && git update-index -q --refresh && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT # capturing original state of files... cp go.mod go.sum "${ORIGINAL_STATE_DIR}" # capturing state of go.mod and go.sum after running go mod tidy... go mod tidy cp go.mod go.sum "${TIDY_STATE_DIR}" set +e # detect difference between the git HEAD state and the go mod tidy state DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod") DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum") if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then echo "go.mod diff:" echo "${DIFF_MOD}" echo "go.sum diff:" echo "${DIFF_SUM}" echo "" printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n" exit 1 fi ================================================ FILE: .github/scripts/json-schema-drift-check.sh ================================================ #!/usr/bin/env bash set -u if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then echo " 🔴 there are uncommitted changes, please commit them before running this check" exit 1 fi if ! make generate-json-schema; then echo "Generating json schema failed" exit 1 fi if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then echo " 🔴 there are uncommitted changes, please commit them before running this check" exit 1 fi ================================================ FILE: .github/scripts/trigger-release.sh ================================================ #!/usr/bin/env bash set -eu bold=$(tput bold) normal=$(tput sgr0) GH_CLI=.tool/gh if ! [ -x "$(command -v $GH_CLI)" ]; then echo "The GitHub CLI could not be found. run: make bootstrap" exit 1 fi # we want to stop the release as early as possible if the version is not a release version ./.github/scripts/check-syft-version-is-release.sh $GH_CLI auth status # set the default repo in cases where multiple remotes are defined $GH_CLI repo set-default anchore/grype export GITHUB_TOKEN="${GITHUB_TOKEN-"$($GH_CLI auth token)"}" # we need all of the git state to determine the next version. Since tagging is done by # the release pipeline it is possible to not have all of the tags from previous releases. git fetch --tags # populates the CHANGELOG.md and VERSION files echo "${bold}Generating changelog...${normal}" make changelog 2> /dev/null NEXT_VERSION=$(cat VERSION) if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then echo "Could not determine the next version to release. Exiting..." exit 1 fi while true; do read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn case $yn in [Yy]* ) echo; break;; [Nn]* ) echo; echo "Cancelling release..."; exit;; * ) echo "Please answer yes or no.";; esac done echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..." echo $GH_CLI workflow run release.yaml -f version=${NEXT_VERSION} echo echo "${bold}Waiting for release to start...${normal}" sleep 10 set +e echo "${bold}Head to the release workflow to monitor the release:${normal} $($GH_CLI run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')" id=$($GH_CLI run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId') $GH_CLI run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" $GH_CLI run view $id --log-failed) ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ name: CodeQL Security Scan on: workflow_dispatch: push: paths: - '**' - '!**.md' - '!LICENSE' - '!test/**' branches: [ main ] schedule: - cron: '0 14 * * 4' jobs: CodeQL: uses: anchore/workflows/.github/workflows/codeql-go.yaml@main with: entrypoint: "./cmd/${{ github.event.repository.name }}" permissions: security-events: write contents: read ================================================ FILE: .github/workflows/dependabot-automation.yaml ================================================ name: Dependabot Automation on: pull_request: permissions: pull-requests: write jobs: run: uses: anchore/workflows/.github/workflows/dependabot-automation.yaml@main ================================================ FILE: .github/workflows/oss-project-board-add.yaml ================================================ name: Add to OSS board permissions: contents: read on: issues: types: - opened - reopened - transferred - labeled jobs: run: uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main" secrets: token: ${{ secrets.OSS_PROJECT_GH_TOKEN }} ================================================ FILE: .github/workflows/release.yaml ================================================ name: "Release" on: workflow_dispatch: inputs: version: description: tag the latest commit on main with the given version (prefixed with v) required: true skip_quality_gate: description: skip quality gate and proceed directly to releasing (for emergency releases) type: boolean default: false permissions: contents: read jobs: quality-gate: if: ${{ !inputs.skip_quality_gate }} environment: release runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Check if running on main if: github.ref != 'refs/heads/main' # we are using the following flag when running `cosign blob-verify` for checksum signature verification: # --certificate-identity-regexp "https://github.com/anchore/.github/workflows/release.yaml@refs/heads/main" # if we are not on the main branch, the signature will not be verifiable since the suffix requires the main branch # at the time of when the OIDC token was issued on the Github Actions runner. run: echo "This can only be run on the main branch otherwise releases produced will not be verifiable with cosign" && exit 1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Check if pinned syft is a release version run: .github/scripts/check-syft-version-is-release.sh - name: Check if tag already exists # note: this will fail if the tag already exists run: | [[ "$VERSION" == v* ]] || (echo "version '$VERSION' does not have a 'v' prefix" && exit 1) git tag "$VERSION" env: VERSION: ${{ github.event.inputs.version }} - name: Check static analysis results uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: static-analysis with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Static analysis" ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check unit test results uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: unit with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Unit tests" ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check integration test results uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: integration with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Integration tests" timeoutSeconds: 1200 # 20 minutes, it sometimes takes that long ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check integration test results uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: quality_tests with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Quality tests" timeoutSeconds: 1200 # 20 minutes, it sometimes takes that long ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check acceptance test results (linux) uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: acceptance-linux with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Acceptance tests (Linux)" ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check acceptance test results (mac) uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: acceptance-mac with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "Acceptance tests (Mac)" ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Check cli test results (linux) uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0 id: cli-linux with: token: ${{ secrets.GITHUB_TOKEN }} # This check name is defined as the github action job name (in .github/workflows/testing.yaml) checkName: "CLI tests (Linux)" ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Quality gate if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.quality_tests.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' env: STATIC_ANALYSIS_STATUS: ${{ steps.static-analysis.conclusion }} UNIT_TEST_STATUS: ${{ steps.unit.outputs.conclusion }} INTEGRATION_TEST_STATUS: ${{ steps.integration.outputs.conclusion }} QUALITY_TEST_STATUS: ${{ steps.quality_tests.outputs.conclusion }} ACCEPTANCE_LINUX_STATUS: ${{ steps.acceptance-linux.outputs.conclusion }} ACCEPTANCE_MAC_STATUS: ${{ steps.acceptance-mac.outputs.conclusion }} CLI_LINUX_STATUS: ${{ steps.cli-linux.outputs.conclusion }} run: | echo "Static Analysis Status: $STATIC_ANALYSIS_STATUS" echo "Unit Test Status: $UNIT_TEST_STATUS" echo "Integration Test Status: $INTEGRATION_TEST_STATUS" echo "Quality Test Status: $QUALITY_TEST_STATUS" echo "Acceptance Test (Linux) Status: $ACCEPTANCE_LINUX_STATUS" echo "Acceptance Test (Mac) Status: $ACCEPTANCE_MAC_STATUS" echo "CLI Test (Linux) Status: $CLI_LINUX_STATUS" false # only release core assets within the "release" job. Any other assets not already under the purview of the # goreleaser configuration should be added as separate jobs to allow for debugging separately from the release workflow # as well as not accidentally be re-run as a step multiple times (as could be done within the release workflow) as # not all actions are guaranteed to be idempotent. release: needs: [quality-gate] if: ${{ always() && (needs.quality-gate.result == 'success' || inputs.skip_quality_gate) }} runs-on: ubuntu-24.04 permissions: contents: write packages: write id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: fetch-depth: 0 persist-credentials: true - name: Bootstrap environment uses: ./.github/actions/bootstrap with: # use the same cache we used for building snapshots build-cache-key-prefix: "snapshot" - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0 with: username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }} password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }} - name: Login to GitHub Container Registry uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Tag release run: | git config user.name "anchoreci" git config user.email "anchoreci@users.noreply.github.com" git tag -a "$VERSION" -m "Release $VERSION" git push origin --tags env: VERSION: ${{ github.event.inputs.version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build & publish release artifacts run: make ci-release env: # for mac signing and notarization... QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }} QUILL_SIGN_PASSWORD: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_PASS }} QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }} QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }} # for creating the release (requires write access to packages and content) GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # for updating brew formula in anchore/homebrew-syft GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} - uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1 continue-on-error: true with: artifact-name: sbom.spdx.json - name: Notify Slack of new release uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1 continue-on-error: true with: webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} webhook-type: incoming-webhook payload: | text: "A new Grype release has been published: https://github.com/anchore/grype/releases/tag/${{ github.event.inputs.version }}" blocks: - type: section text: type: mrkdwn text: | *A new Grype release has been published* :rocket: • Release: • Repo: `${{ github.repository }}` • Workflow: `${{ github.workflow }}` • Event: `${{ github.event_name }}` if: ${{ success() }} release-install-script: needs: [release] if: ${{ needs.release.result == 'success' }} uses: "anchore/workflows/.github/workflows/release-install-script.yaml@main" with: tag: ${{ github.event.inputs.version }} secrets: # needed for r2... R2_INSTALL_ACCESS_KEY_ID: ${{ secrets.OSS_R2_INSTALL_ACCESS_KEY_ID }} R2_INSTALL_SECRET_ACCESS_KEY: ${{ secrets.OSS_R2_INSTALL_SECRET_ACCESS_KEY }} R2_ENDPOINT: ${{ secrets.TOOLBOX_CLOUDFLARE_R2_ENDPOINT }} # needed for s3... S3_INSTALL_AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} S3_INSTALL_AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} ================================================ FILE: .github/workflows/remove-awaiting-response-label.yaml ================================================ name: "Manage Awaiting Response Label" on: issue_comment: types: [created] jobs: run: permissions: issues: write pull-requests: write uses: "anchore/workflows/.github/workflows/remove-awaiting-response-label.yaml@main" secrets: token: ${{ secrets.OSS_PROJECT_GH_TOKEN }} ================================================ FILE: .github/workflows/scorecards.yml ================================================ name: Scorecards supply-chain security on: # Only the default branch is supported. branch_protection_rule: push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecards analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Used to receive a badge. id-token: write steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # Publish the results for public repositories to enable scorecard badges. For more details, see # https://github.com/ossf/scorecard-action#publishing-results. # For private repositories, `publish_results` will automatically be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/update-anchore-dependencies.yml ================================================ name: PR to update Anchore dependencies on: workflow_dispatch: inputs: repos: description: "List of dependencies to update" required: true type: string permissions: contents: read jobs: update: runs-on: ubuntu-latest if: github.repository_owner == 'anchore' # only run for main repo (not forks) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap with: tools: false bootstrap-apt-packages: "" - name: Update dependencies id: update uses: anchore/workflows/.github/actions/update-go-dependencies@main with: repos: ${{ github.event.inputs.repos }} - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0 id: generate-token with: app_id: ${{ secrets.TOKEN_APP_ID }} private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0 with: signoff: true delete-branch: true draft: ${{ steps.update.outputs.draft }} # do not change this branch, as other workflows depend on it branch: auto/integration labels: dependencies,pre-release commit-message: "chore(deps): update anchore dependencies" title: "chore(deps): update anchore dependencies" body: ${{ steps.update.outputs.summary }} token: ${{ steps.generate-token.outputs.token }} ================================================ FILE: .github/workflows/update-bootstrap-tools.yml ================================================ name: PR for latest versions of tools on: schedule: - cron: "0 8 * * *" # 3 AM EST workflow_dispatch: permissions: contents: read jobs: update-bootstrap-tools: runs-on: ubuntu-latest if: github.repository == 'anchore/grype' # only run for main repo steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap with: bootstrap-apt-packages: "" compute-fingerprints: "false" go-dependencies: false - name: "Update tool versions" id: latest-versions run: | make update-tools make list-tools export NO_COLOR=1 delimiter="$(openssl rand -hex 8)" { echo "status<<${delimiter}" make list-tool-updates echo "${delimiter}" } >> $GITHUB_OUTPUT { echo "### Tool version status" echo "\`\`\`" make list-tool-updates echo "\`\`\`" } >> $GITHUB_STEP_SUMMARY - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0 id: generate-token with: app_id: ${{ secrets.TOKEN_APP_ID }} private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0 with: signoff: true delete-branch: true branch: auto/latest-tools labels: dependencies commit-message: 'chore(deps): update tools to latest versions' title: 'chore(deps): update tools to latest versions' body: | ``` ${{ steps.latest-versions.outputs.status }} ``` This is an auto-generated pull request to update all of the tools to the latest versions. token: ${{ steps.generate-token.outputs.token }} ================================================ FILE: .github/workflows/update-generated-code.yml ================================================ name: PR to update OS codename generated code on: schedule: - cron: "0 1 * * 1" # every Monday at 1 AM UTC workflow_dispatch: permissions: contents: read env: SLACK_NOTIFICATIONS: true jobs: run-code-gen: name: "Run code generation" runs-on: ubuntu-latest if: github.repository == 'anchore/grype' # only run for main repo steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap with: bootstrap-apt-packages: "" compute-fingerprints: "false" go-dependencies: true - name: "Generate codename data" run: | make generate-codename-data - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0 id: generate-token with: app_id: ${{ secrets.TOKEN_APP_ID }} private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0 with: signoff: true delete-branch: true branch: auto/latest-codename-data labels: dependencies commit-message: "chore(deps): update OS codename generated code" title: "chore(deps): update OS codename generated code" body: | Update OS codename data from endoflife.date token: ${{ steps.generate-token.outputs.token }} - name: Notify Slack on failure uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1 if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} with: webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} webhook-type: incoming-webhook payload: | text: "Grype OS codename code generation failed" blocks: - type: section text: type: mrkdwn text: | *Grype OS codename code generation failed* • Workflow: `${{ github.workflow }}` • Event: `${{ github.event_name }}` • Job Status: `${{ job.status }}` • <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run> ================================================ FILE: .github/workflows/update-quality-gate-db.yml ================================================ name: PR for upgrading quality gate test DB on: schedule: - cron: "0 16 1 * *" # first day of each month @ 11 AM EST workflow_dispatch: permissions: contents: read jobs: update-test-db-url: runs-on: ubuntu-latest if: github.repository == 'anchore/grype' # only run for main repo steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: "Update quality DB" run: | make update-quality-gate-db - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0 id: generate-token with: app_id: ${{ secrets.TOKEN_APP_ID }} private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0 with: signoff: true delete-branch: true branch: auto/update-quality-test-db labels: test, changelog-ignore commit-message: 'test: update quality gate db to latest version' title: 'test: update quality gate db to latest version' body: | This is an auto-generated pull request to update the quality gate db to latest version token: ${{ steps.generate-token.outputs.token }} ================================================ FILE: .github/workflows/validate-github-actions.yaml ================================================ name: "Validate GitHub Actions" on: pull_request: paths: - '.github/workflows/**' - '.github/actions/**' push: branches: - main paths: - '.github/workflows/**' - '.github/actions/**' permissions: contents: read jobs: zizmor: name: "Lint" runs-on: ubuntu-latest permissions: contents: read security-events: write # for uploading SARIF results steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run zizmor" uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 with: config: .github/zizmor.yml # Disable SARIF upload so the step is a simple pass/fail gate advanced-security: false inputs: .github ================================================ FILE: .github/workflows/validations.yaml ================================================ name: "Validations" on: workflow_dispatch: pull_request: push: branches: - main permissions: contents: read jobs: Static-Analysis: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Static analysis" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Run static analysis run: make static-analysis Unit-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Unit tests" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Run unit tests run: make unit Quality-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Quality tests" runs-on: ubuntu-22.04-4core-16gb steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Run quality tests run: make quality env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Archive the provider state if: ${{ failure() }} run: tar -czvf qg-capture-state.tar.gz -C test/quality --exclude tools --exclude labels .yardstick.yaml .yardstick - name: Upload the provider state archive if: ${{ failure() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: qg-capture-state path: qg-capture-state.tar.gz - name: Show instructions to debug if: ${{ failure() }} run: | ARCHIVE_BASENAME=qg-capture-state ARCHIVE_NAME=$ARCHIVE_BASENAME.zip cat << EOF >> $GITHUB_STEP_SUMMARY ## Troubleshooting failed run Download the artifact from this workflow run: \`$ARCHIVE_NAME\` Then run the following commands to debug: \`\`\`bash # copy the archive to the tests/quality directory cd test/quality unzip $ARCHIVE_NAME && tar -xzf $ARCHIVE_BASENAME.tar.gz \`\`\` Now you can debug the with yardstick: \`\`\`bash poetry shell yardstick result list yardstick label explore \`\`\` EOF Integration-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Integration tests" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Restore integration test cache uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: ${{ github.workspace }}/test/integration/testdata/cache key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/testdata/cache.fingerprint') }} - name: Run integration tests run: make integration Build-Snapshot-Artifacts: name: "Build snapshot artifacts" runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap with: # why have another build cache key? We don't want unit/integration/etc test build caches to replace # the snapshot build cache, which includes builds for all OSs and architectures. As long as this key is # unique from the build-cache-key-prefix in other CI jobs, we should be fine. # # note: ideally this value should match what is used in release (just to help with build times). build-cache-key-prefix: "snapshot" bootstrap-apt-packages: "" - name: Build snapshot artifacts run: make snapshot # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach). # see https://github.com/actions/upload-artifact/issues/199 for more info - name: Upload snapshot artifacts uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} Upload-Snapshot-Artifacts: name: "Upload snapshot artifacts" needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Download snapshot build uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - run: npm install @actions/artifact@2.2.2 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 with: script: | const { readdirSync } = require('fs') const { DefaultArtifactClient } = require('@actions/artifact') const artifact = new DefaultArtifactClient() const ls = d => readdirSync(d, { withFileTypes: true }) const baseDir = "./snapshot" const dirs = ls(baseDir).filter(f => f.isDirectory()).map(f => f.name) const uploads = [] for (const dir of dirs) { // uploadArtifact returns Promise<{id, size}> uploads.push(artifact.uploadArtifact( // name of the archive: `${dir}`, // array of all files to include: ls(`${baseDir}/${dir}`).map(f => `${baseDir}/${dir}/${f.name}`), // base directory to trim from entries: `${baseDir}/${dir}`, { retentionDays: 30 } )) } // wait for all uploads to finish Promise.all(uploads) Acceptance-Linux: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Acceptance tests (Linux)" needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Download snapshot build uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - name: Restore install.sh test image cache id: install-test-image-cache uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: ${{ github.workspace }}/test/install/cache key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }} - name: Load test image cache if: steps.install-test-image-cache.outputs.cache-hit == 'true' run: make install-test-cache-load - name: Run install.sh tests (Linux) run: make install-test - name: (cache-miss) Create test image cache if: steps.install-test-image-cache.outputs.cache-hit != 'true' run: make install-test-cache-save Acceptance-Mac: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Acceptance tests (Mac)" needs: [Build-Snapshot-Artifacts] runs-on: macos-latest steps: - name: Install Cosign uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 #v4.1.0 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Download snapshot build uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - name: Restore docker image cache for compare testing id: mac-compare-testing-cache uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: image.tar key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }} - name: Run install.sh tests (Mac) run: make install-test-ci-mac Cli-Linux: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "CLI tests (Linux)" needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-24.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Bootstrap environment uses: ./.github/actions/bootstrap - name: Restore CLI test-fixture cache uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: ${{ github.workspace }}/test/cli/testdata/cache key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/testdata/cache.fingerprint') }} - name: Download snapshot build uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3 with: path: snapshot key: snapshot-build-${{ github.run_id }} - name: Run CLI Tests (Linux) run: make cli env: GRYPE_SNAPSHOT_PREBUILT: "true" GRYPE_BINARY_LOCATION: ${{ github.workspace }}/snapshot/linux-build_linux_amd64_v1/grype Cleanup-Cache: name: "Cleanup snapshot cache" if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-24.04 permissions: actions: write needs: - Acceptance-Linux - Acceptance-Mac - Cli-Linux - Upload-Snapshot-Artifacts steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 with: persist-credentials: false - name: Delete snapshot cache run: gh cache delete "snapshot-build-${{ github.run_id }}" || echo "Cache deletion failed or cache not found - continuing" env: GH_TOKEN: ${{ github.token }} ================================================ FILE: .github/zizmor.yml ================================================ rules: unpinned-uses: config: policies: # anchore/workflows is an internal repository; using @main is acceptable anchore/*: any ================================================ FILE: .gitignore ================================================ # AI .claude CLAUDE.MD # local development tailoring go.work go.work.sum .tool-versions .jj/ mise.toml specs/ *.xxh64 # AI and LLM related files CLAUDE.md .claude # app configuration /.grype.yaml # tool and bin directories .tmp/ bin/ /bin /.bin /build /dist /snapshot /.tool /.task # changelog generation /CHANGELOG.md /VERSION # IDE configuration .vscode/ .idea/ .server/ .history/ # test related *.fingerprint /test/results coverage.txt *.log .server # grype-db related /metadata.json /listing.json *.db *.db-journal !**/testdata/**/*.db !**/testdata/**/bin/ !**/testdata/**/*.jar # probable archives .images *.tar *.jar *.war *.ear *.jpi *.hpi *.zip *.iml # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # OS files .DS_Store *.profile # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib /grype/grype main # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out ================================================ FILE: .gitmodules ================================================ [submodule "test/quality/vulnerability-match-labels"] path = test/quality/vulnerability-match-labels url = https://github.com/anchore/vulnerability-match-labels.git branch = main ================================================ FILE: .golangci.yaml ================================================ version: "2" linters: # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint default: none enable: - asciicheck - bodyclose - copyloopvar - dogsled - dupl - errcheck - funlen - gocognit - goconst - gocritic - gocyclo - goprintffuncname - gosec - govet - ineffassign - misspell - nakedret - revive - staticcheck - unconvert - unparam - unused - whitespace settings: funlen: lines: 70 statements: 50 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: # we have multiple packages in grype that might overlap with the stblib; their names reflect their purpose - linters: - revive text: "var-naming: avoid package names that conflict" paths: - third_party$ - builtin$ - examples$ # do not enable... # - deadcode # The owner seems to have abandoned the linter. Replaced by "unused". # - depguard # we need to setup a configuration for this # - goprintffuncname # does not catch all cases and there are exceptions # - nakedret # does not catch all cases and should not fail a build # - gochecknoglobals # - gochecknoinits # this is too aggressive # - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649 # - godot # - godox # - goerr113 # - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818) # - golint # deprecated # - gomnd # this is too aggressive # - interfacer # this is a good idea, but is no longer supported and is prone to false positives # - lll # without a way to specify per-line exception cases, this is not usable # - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations # - nestif # - nolintlint # as of go1.19 this conflicts with the behavior of gofmt, which is a deal-breaker (lint-fix will still fail when running lint) # - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code # - rowserrcheck # not in a repo with sql, so this is not useful # - scopelint # deprecated # - structcheck # The owner seems to have abandoned the linter. Replaced by "unused". # - testpackage # - varcheck # The owner seems to have abandoned the linter. Replaced by "unused". # - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) issues: max-same-issues: 25 uniq-by-line: false # TODO: enable this when we have coverage on docstring comments # # The list of ids of default excludes to include or disable. # include: # - EXC0002 # disable excluding of issues about comments from golint formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .goreleaser.yaml ================================================ version: 2 release: prerelease: auto draft: false env: # required to support multi architecture docker builds - DOCKER_CLI_EXPERIMENTAL=enabled - CGO_ENABLED=0 builds: - id: linux-build dir: ./cmd/grype binary: grype goos: - linux goarch: - amd64 - arm64 - ppc64le - s390x # set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}' ldflags: &build-ldflags | -w -s -extldflags '-static' -X main.version={{.Version}} -X main.gitCommit={{.Commit}} -X main.buildDate={{.Date}} -X main.gitDescription={{.Summary}} - id: darwin-build dir: ./cmd/grype binary: grype goos: - darwin goarch: - amd64 - arm64 mod_timestamp: *build-timestamp ldflags: *build-ldflags hooks: post: - cmd: .tool/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv env: - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log - id: windows-build dir: ./cmd/grype binary: grype goos: - windows goarch: - amd64 mod_timestamp: *build-timestamp ldflags: *build-ldflags archives: - id: linux-archives ids: - linux-build - id: darwin-archives ids: - darwin-build - id: windows-archives formats: [zip] ids: - windows-build nfpms: - license: "Apache 2.0" maintainer: "Anchore, Inc" homepage: &website "https://github.com/anchore/grype" description: &description "A vulnerability scanner for container images and filesystems" formats: - rpm - deb homebrew_casks: - repository: owner: anchore name: homebrew-grype token: "{{.Env.GITHUB_BREW_TOKEN}}" ids: - darwin-archives - linux-archives homepage: *website description: *description license: "Apache License 2.0" conflicts: # claim that the previous grype formula (in the root of homebrew-grype) conflicts with the new cask # see https://goreleaser.com/deprecations/#brews for more information - formula: grype dockers: # production images... - image_templates: - anchore/grype:{{.Tag}}-amd64 - ghcr.io/anchore/grype:{{.Tag}}-amd64 goarch: amd64 dockerfile: Dockerfile use: buildx build_flag_templates: - "--platform=linux/amd64" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-arm64v8 goarch: arm64 dockerfile: Dockerfile use: buildx build_flag_templates: - "--platform=linux/arm64/v8" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-ppc64le goarch: ppc64le dockerfile: Dockerfile use: buildx build_flag_templates: - "--platform=linux/ppc64le" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-s390x - ghcr.io/anchore/grype:{{.Tag}}-s390x goarch: s390x dockerfile: Dockerfile use: buildx build_flag_templates: - "--platform=linux/s390x" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" # nonroot images... - image_templates: - anchore/grype:{{.Tag}}-nonroot-amd64 - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64 goarch: amd64 dockerfile: Dockerfile.nonroot use: buildx build_flag_templates: - "--platform=linux/amd64" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-nonroot-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8 goarch: arm64 dockerfile: Dockerfile.nonroot use: buildx build_flag_templates: - "--platform=linux/arm64/v8" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-nonroot-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le goarch: ppc64le dockerfile: Dockerfile.nonroot use: buildx build_flag_templates: - "--platform=linux/ppc64le" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-nonroot-s390x - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x goarch: s390x dockerfile: Dockerfile.nonroot use: buildx build_flag_templates: - "--platform=linux/s390x" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" # debug images... - image_templates: - anchore/grype:{{.Tag}}-debug-amd64 - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64 goarch: amd64 dockerfile: Dockerfile.debug use: buildx build_flag_templates: - "--platform=linux/amd64" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-debug-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8 goarch: arm64 dockerfile: Dockerfile.debug use: buildx build_flag_templates: - "--platform=linux/arm64/v8" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-debug-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le goarch: ppc64le dockerfile: Dockerfile.debug use: buildx build_flag_templates: - "--platform=linux/ppc64le" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" - image_templates: - anchore/grype:{{.Tag}}-debug-s390x - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x goarch: s390x dockerfile: Dockerfile.debug use: buildx build_flag_templates: - "--platform=linux/s390x" - "--provenance=false" - "--build-arg=BUILD_DATE={{.Date}}" - "--build-arg=BUILD_VERSION={{.Version}}" - "--build-arg=VCS_REF={{.FullCommit}}" - "--build-arg=VCS_URL={{.GitURL}}" docker_manifests: - name_template: anchore/grype:latest image_templates: - anchore/grype:{{.Tag}}-amd64 - anchore/grype:{{.Tag}}-arm64v8 - anchore/grype:{{.Tag}}-ppc64le - anchore/grype:{{.Tag}}-s390x - name_template: ghcr.io/anchore/grype:latest image_templates: - ghcr.io/anchore/grype:{{.Tag}}-amd64 - ghcr.io/anchore/grype:{{.Tag}}-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-s390x - name_template: anchore/grype:{{.Tag}} image_templates: - anchore/grype:{{.Tag}}-amd64 - anchore/grype:{{.Tag}}-arm64v8 - anchore/grype:{{.Tag}}-ppc64le - anchore/grype:{{.Tag}}-s390x - name_template: ghcr.io/anchore/grype:{{.Tag}} image_templates: - ghcr.io/anchore/grype:{{.Tag}}-amd64 - ghcr.io/anchore/grype:{{.Tag}}-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-s390x # nonroot images... - name_template: anchore/grype:nonroot image_templates: - anchore/grype:{{.Tag}}-nonroot-amd64 - anchore/grype:{{.Tag}}-nonroot-arm64v8 - anchore/grype:{{.Tag}}-nonroot-ppc64le - anchore/grype:{{.Tag}}-nonroot-s390x - name_template: ghcr.io/anchore/grype:nonroot image_templates: - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64 - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x - name_template: anchore/grype:{{.Tag}}-nonroot image_templates: - anchore/grype:{{.Tag}}-nonroot-amd64 - anchore/grype:{{.Tag}}-nonroot-arm64v8 - anchore/grype:{{.Tag}}-nonroot-ppc64le - anchore/grype:{{.Tag}}-nonroot-s390x - name_template: ghcr.io/anchore/grype:{{.Tag}}-nonroot image_templates: - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64 - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x # debug images... - name_template: anchore/grype:debug image_templates: - anchore/grype:{{.Tag}}-debug-amd64 - anchore/grype:{{.Tag}}-debug-arm64v8 - anchore/grype:{{.Tag}}-debug-ppc64le - anchore/grype:{{.Tag}}-debug-s390x - name_template: ghcr.io/anchore/grype:debug image_templates: - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64 - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x - name_template: anchore/grype:{{.Tag}}-debug image_templates: - anchore/grype:{{.Tag}}-debug-amd64 - anchore/grype:{{.Tag}}-debug-arm64v8 - anchore/grype:{{.Tag}}-debug-ppc64le - anchore/grype:{{.Tag}}-debug-s390x - name_template: ghcr.io/anchore/grype:{{.Tag}}-debug image_templates: - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64 - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8 - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x signs: - cmd: .tool/cosign signature: "${artifact}.sig" certificate: "${artifact}.pem" args: - "sign-blob" - "--use-signing-config=false" - "--oidc-issuer=https://token.actions.githubusercontent.com" - "--output-certificate=${certificate}" - "--output-signature=${signature}" - "${artifact}" - "--yes" artifacts: checksum ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct All contributors for any Anchore project must follow the [Contributor Covenant Code of Conduct](https://oss.anchore.com/docs/contributing/code-of-conduct/). **TLDR:** Be kind, be respectful, and assume good intentions. We're all here to build great software together. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thank you for your interest in contributing to Grype! Please see the [contribution guide](https://oss.anchore.com/docs/contributing/grype/) for development requirements and helpful tips to get started developing in the repo. For a deeper dive, please see the [architecture docs for grype](https://oss.anchore.com/docs/architecture/grype/) and the sister project [grype-db](https://oss.anchore.com/docs/architecture/grype-db/). **Have a question or need help?** Check out our [issues and discussions guide](https://oss.anchore.com/docs/contributing/issues-and-discussions/) to find the right place to start a conversation. **Ready to submit code?** Our [pull request guide](https://oss.anchore.com/docs/contributing/pull-requests/) covers everything from title conventions to the review process. Don't forget that ***all commits require a [sign-off](https://oss.anchore.com/docs/contributing/sign-off/)***. **Found a security issue?** Please do **not** open a public issue. Instead, see our [security policy](https://oss.anchore.com/docs/contributing/security/) for how to report vulnerabilities responsibly. **Want to help improve the docs?** Check out the [anchore/oss-docs](https://github.com/anchore/oss-docs) repository. ================================================ FILE: Dockerfile ================================================ FROM gcr.io/distroless/static-debian12:latest AS build FROM scratch # needed for version check HTTPS request COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt # create the /tmp dir, which is needed for image content cache WORKDIR /tmp COPY grype / ARG BUILD_DATE ARG BUILD_VERSION ARG VCS_REF ARG VCS_URL LABEL org.opencontainers.image.created=$BUILD_DATE LABEL org.opencontainers.image.title="grype" LABEL org.opencontainers.image.description="A vulnerability scanner for container images and filesystems" LABEL org.opencontainers.image.source=$VCS_URL LABEL org.opencontainers.image.revision=$VCS_REF LABEL org.opencontainers.image.vendor="Anchore, Inc." LABEL org.opencontainers.image.version=$BUILD_VERSION LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/grype/main/README.md" LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png" LABEL io.artifacthub.package.license="Apache-2.0" ENTRYPOINT ["/grype"] ================================================ FILE: Dockerfile.debug ================================================ FROM gcr.io/distroless/static-debian12:debug-nonroot # create the /tmp dir, which is needed for image content cache WORKDIR /tmp COPY grype / ARG BUILD_DATE ARG BUILD_VERSION ARG VCS_REF ARG VCS_URL LABEL org.opencontainers.image.created=$BUILD_DATE LABEL org.opencontainers.image.title="grype" LABEL org.opencontainers.image.description="A vulnerability scanner for container images and filesystems" LABEL org.opencontainers.image.source=$VCS_URL LABEL org.opencontainers.image.revision=$VCS_REF LABEL org.opencontainers.image.vendor="Anchore, Inc." LABEL org.opencontainers.image.version=$BUILD_VERSION LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/grype/main/README.md" LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png" LABEL io.artifacthub.package.license="Apache-2.0" ENTRYPOINT ["/grype"] ================================================ FILE: Dockerfile.nonroot ================================================ FROM gcr.io/distroless/static-debian12:nonroot # create the /tmp dir, which is needed for image content cache WORKDIR /tmp COPY grype / ARG BUILD_DATE ARG BUILD_VERSION ARG VCS_REF ARG VCS_URL LABEL org.opencontainers.image.created=$BUILD_DATE LABEL org.opencontainers.image.title="grype" LABEL org.opencontainers.image.description="A vulnerability scanner for container images and filesystems" LABEL org.opencontainers.image.source=$VCS_URL LABEL org.opencontainers.image.revision=$VCS_REF LABEL org.opencontainers.image.vendor="Anchore, Inc." LABEL org.opencontainers.image.version=$BUILD_VERSION LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/grype/main/README.md" LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png" LABEL io.artifacthub.package.license="Apache-2.0" ENTRYPOINT ["/grype"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ TOOL_DIR = .tool BINNY = $(TOOL_DIR)/binny TASK = $(TOOL_DIR)/task .DEFAULT_GOAL := make-default ## Bootstrapping targets ################################# # note: we need to assume that binny and task have not already been installed $(BINNY): @mkdir -p $(TOOL_DIR) @curl -sSfL https://get.anchore.io/binny | sh -s -- -b $(TOOL_DIR) # note: we need to assume that binny and task have not already been installed .PHONY: task $(TASK) task: $(BINNY) @$(BINNY) install task -q .PHONY: ci-bootstrap-go ci-bootstrap-go: go mod download # this is a bootstrapping catch-all, where if the target doesn't exist, we'll ensure the tools are installed and then try again %: make $(TASK) $(TASK) $@ ## Shim targets ################################# .PHONY: make-default make-default: $(TASK) @# run the default task in the taskfile @$(TASK) # for those of us that can't seem to kick the habit of typing `make ...` lets wrap the superior `task` tool TASKS := $(shell bash -c "test -f $(TASK) && NO_COLOR=1 $(TASK) -l | grep '^\* ' | cut -d' ' -f2 | tr -d ':' | tr '\n' ' '" ) $(shell bash -c "test -f $(TASK) && NO_COLOR=1 $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\n' ' ' | tr -d ','") .PHONY: $(TASKS) $(TASKS): $(TASK) @$(TASK) $@ help: $(TASK) @$(TASK) -l ================================================ FILE: README.md ================================================

Grype logo

# Grype **A vulnerability scanner for container images and filesystems.**

 Static Analysis + Unit + Integration   Validations   Go Report Card   GitHub release   GitHub go.mod Go version   License: Apache-2.0   Join our Discourse   Follow on Mastodon 

![grype-demo](https://user-images.githubusercontent.com/590471/90276236-9868f300-de31-11ea-8068-4268b6b68529.gif) ## Features - Scan **container images**, **filesystems**, and **SBOMs** for known vulnerabilities (see the docs for a full list of [supported scan targets](https://oss.anchore.com/docs/guides/vulnerability/scan-targets/)) - Supports major OS package ecosystems (Alpine, Debian, Ubuntu, RHEL, Oracle Linux, Amazon Linux, and [more](https://oss.anchore.com/docs/capabilities/all-os/)) - Supports language-specific packages (Ruby, Java, JavaScript, Python, .NET, Go, PHP, Rust, and [more](https://oss.anchore.com/docs/capabilities/all-packages/)) - Supports Docker, OCI, and [Singularity](https://github.com/sylabs/singularity) image formats - Threat & risk prioritization with **EPSS**, **KEV**, and **risk scoring** (see [interpreting the results docs](https://oss.anchore.com/docs/guides/vulnerability/interpreting-results/)) - [OpenVEX](https://github.com/openvex) support for filtering and augmenting scan results > [!TIP] > New to Grype? Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/vulnerability/getting-started/) for a walkthrough! ## Installation The quickest way to get up and going: ```bash curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin ``` > [!TIP] > See [Installation docs](https://oss.anchore.com/docs/installation/grype/) for more ways to get Grype, including Homebrew, Docker, Chocolatey, MacPorts, and more! ## The basics Scan a container image or directory for vulnerabilities: ```bash # container image grype alpine:latest # directory grype ./my-project ``` Scan an SBOM for even faster vulnerability detection: ```bash # scan a Syft SBOM grype sbom:./sbom.json # pipe an SBOM into Grype cat ./sbom.json | grype ``` > [!TIP] > Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/vulnerability/getting-started/) to explore all of the capabilities and features. > > Want to know all of the ins-and-outs of Grype? Check out the [CLI docs](https://oss.anchore.com/docs/reference/grype/cli/) and [configuration docs](https://oss.anchore.com/docs/reference/grype/configuration/). ## Contributing We encourage users to help make these tools better by [submitting issues](https://github.com/anchore/grype/issues) when you find a bug or want a new feature. Check out our [contributing overview](https://oss.anchore.com/docs/contributing/) and [developer-specific documentation](https://oss.anchore.com/docs/contributing/grype/) if you are interested in providing code contributions.

Grype development is sponsored by Anchore, and is released under the Apache-2.0 License. The Grype logo by Anchore is licensed under CC BY 4.0

For commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/). ## Come talk to us! The Grype Team holds regular community meetings online. All are welcome to join to bring topics for discussion. - Check the [calendar](https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) for the next meeting date. - Add items to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing) (join [this group](https://groups.google.com/g/anchore-oss-community) for write access to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing)) - See you there! ================================================ FILE: RELEASE.md ================================================ # Release A release of grype comprises: - a new semver git tag from the current tip of the main branch - a new [github release](https://github.com/anchore/grype/releases) with a changelog and archived binary assets - docker images published to `ghcr.io` and `dockerhub`, including multi architecture images + manifest - [`anchore/homebrew-grype`](https://github.com/anchore/homebrew-grype) tap updated to point to assets in the latest github release Ideally releasing should be done often with small increments when possible. Unless a breaking change is blocking the release, or no fixes/features have been merged, a good target release cadence is between every 1 or 2 weeks. ## Creating a release This release process itself should be as automated as possible, and has only a few steps: 1. **Trigger a new release with `make release`**. At this point you'll see a preview changelog in the terminal. If you're happy with the changelog, press `y` to continue, otherwise you can abort and adjust the labels on the PRs and issues to be included in the release and re-run the release trigger command. 1. A release admin must approve the release on the GitHub Actions [release pipeline](https://github.com/anchore/grype/actions/workflows/release.yaml) run page. Once approved, the release pipeline will generate all assets and publish a GitHub Release. ## Retracting a release If a release is found to be problematic, it can be retracted with the following steps: - Deleting the GitHub Release - Untag the docker images in the `ghcr.io` and `docker.io` registries - Revert the brew formula in [`anchore/homebrew-grype`](https://github.com/anchore/homebrew-grype) to point to the previous release - Add a new `retract` entry in the go.mod for the versioned release **Note**: do not delete release tags from the git repository since there may already be references to the release in the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there will be a warning when users try to pull the new release). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Security updates are applied only to the most recent release, try to always be up to date. ## Reporting a Vulnerability To report a security issue, please email [security@anchore.com](mailto:security@anchore.com) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. All support will be made on a best effort basis, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low. For more details, see our [security policy documentation](https://oss.anchore.com/docs/contributing/security/). ================================================ FILE: Taskfile.yaml ================================================ version: "3" vars: OWNER: anchore PROJECT: grype # static file dirs TOOL_DIR: .tool TMP_DIR: .tmp # used for changelog generation CHANGELOG: CHANGELOG.md NEXT_VERSION: VERSION # used for snapshot builds OS: sh: uname -s | tr '[:upper:]' '[:lower:]' ARCH: sh: | [ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || echo $(uname -m) PROJECT_ROOT: sh: echo $PWD # note: the snapshot dir must be a relative path starting with ./ SNAPSHOT_DIR: ./snapshot SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}" SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign" BUILD_CMD: "{{ .TOOL_DIR }}/goreleaser build --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --single-target" RELEASE_CMD: "{{ .TOOL_DIR }}/goreleaser release --clean --release-notes {{ .CHANGELOG }}" VERSION: sh: git describe --dirty --always --tags # used for install and acceptance testing COMPARE_DIR: ./test/compare COMPARE_TEST_IMAGE: centos:8.2.2004 env: SYFT_CHECK_FOR_APP_UPDATE: false GRYPE_CHECK_FOR_APP_UPDATE: false tasks: ## High-level tasks ################################# # note: the default task should not run levels of test, only build the project. default: desc: Build the project cmds: - task: build validate: desc: Run all validation tasks aliases: - pr-validations - validations cmds: - task: static-analysis - task: test - task: install-test static-analysis: desc: Run all static analysis tasks cmds: - task: check-go-mod-tidy - task: check-licenses - task: lint - task: check-json-schema-drift - task: check-db-schema-drift # TODO: while developing v6, we need to disable this check (since v5 and v6 are imported in the same codebase) # - task: validate-grype-db-schema test: desc: Run all levels of test cmds: - task: unit - task: integration - task: cli ## Bootstrap tasks ################################# binny: internal: true # desc: Get the binny tool generates: - "{{ .TOOL_DIR }}/binny" status: - "test -f {{ .TOOL_DIR }}/binny" cmd: "curl -sSfL https://get.anchore.io/binny | sh -s -- -b .tool" silent: true tools: desc: Install all tools needed for CI and local development deps: [binny] aliases: - bootstrap generates: - ".binny.yaml" - "{{ .TOOL_DIR }}/*" status: - "{{ .TOOL_DIR }}/binny check -v" cmd: "{{ .TOOL_DIR }}/binny install -v" silent: true update-tools: desc: Update pinned versions of all tools to their latest available versions deps: [binny] generates: - ".binny.yaml" - "{{ .TOOL_DIR }}/*" cmd: "{{ .TOOL_DIR }}/binny update -v" silent: true update-quality-gate-db: desc: Update pinned version of quality gate database cmds: - cmd: "go run cmd/grype/main.go db list -o json | jq -r '\"https://grype.anchore.io/databases/v6/\" + .[0].path' > test/quality/test-db" silent: true list-tools: desc: List all tools needed for CI and local development deps: [binny] cmd: "{{ .TOOL_DIR }}/binny list" silent: true list-tool-updates: desc: List all tools that are not up to date relative to the binny config deps: [binny] cmd: "{{ .TOOL_DIR }}/binny list --updates" silent: true tmpdir: silent: true generates: - "{{ .TMP_DIR }}" cmd: "mkdir -p {{ .TMP_DIR }}" ## Static analysis tasks ################################# format: desc: Auto-format all source code deps: [tools] cmds: - gofmt -w -s . - "{{ .TOOL_DIR }}/gosimports -local github.com/anchore -w ." - go mod tidy lint-fix: desc: Auto-format all source code + run golangci lint fixers deps: [tools] cmds: - task: format - "{{ .TOOL_DIR }}/golangci-lint run --tests=false --fix" lint: desc: Run gofmt + golangci lint checks vars: BAD_FMT_FILES: sh: gofmt -l -s . BAD_FILE_NAMES: sh: "find . | grep -e ':' | grep -v -e 'test/quality/.yardstick' -e 'test/quality/vulnerability-match-labels' || true" deps: [tools] cmds: # ensure there are no go fmt differences - cmd: 'test -z "{{ .BAD_FMT_FILES }}" || (echo "files with gofmt issues: [{{ .BAD_FMT_FILES }}]"; exit 1)' silent: true # ensure there are no files with ":" in it (a known back case in the go ecosystem) - cmd: 'test -z "{{ .BAD_FILE_NAMES }}" || (echo "files with bad names: [{{ .BAD_FILE_NAMES }}]"; exit 1)' silent: true # run linting - "{{ .TOOL_DIR }}/golangci-lint run --tests=false" check-licenses: # desc: Ensure transitive dependencies are compliant with the current license policy deps: [tools] cmd: "{{ .TOOL_DIR }}/bouncer check ./..." check-go-mod-tidy: # desc: Ensure go.mod and go.sum are up to date cmds: - cmd: .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!" silent: true check-json-schema-drift: desc: Ensure there is no drift between the JSON schema and the code cmds: - .github/scripts/json-schema-drift-check.sh check-db-schema-drift: desc: Ensure there is no drift between the database blob schemas and the code cmds: - .github/scripts/db-schema-drift-check.sh validate-grype-db-schema: desc: Ensure the codebase is only referencing a single grype-db schema version (multiple is not allowed) cmds: - python test/validate-grype-db-schema.py ## Testing tasks ################################# unit: desc: Run unit tests deps: - tmpdir vars: TEST_PKGS: sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | grep -v {{ .OWNER }}/{{ .PROJECT }}/internal/test | tr '\n' ' '" # unit test coverage threshold (in % coverage) COVERAGE_THRESHOLD: 47 cmds: - "go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}" - cmd: ".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt" silent: true integration: desc: Run integration tests cmds: - "go test -v ./test/integration" # update database outside race detector, since doing so with # race detector on is very slow - "go run cmd/{{ .PROJECT }}/main.go db update" # exercise most of the CLI with the data race detector enabled - "go run -race cmd/{{ .PROJECT }}/main.go alpine:latest" _ensure-snapshot: # Ensure the snapshot binary is available, building only if needed. # In CI, set GRYPE_SNAPSHOT_PREBUILT=true to skip the rebuild when the binary was restored from cache. # Locally, this falls through to the snapshot task which uses checksum-based staleness detection # so that code changes always produce a fresh binary. internal: true status: - test -n "$GRYPE_SNAPSHOT_PREBUILT" -a -f "{{ .SNAPSHOT_BIN }}" cmds: - task: snapshot cli: desc: Run CLI tests deps: [tools, _ensure-snapshot] sources: - "{{ .SNAPSHOT_BIN }}" - ./test/cli/** - ./**/*.go cmds: - cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'" silent: true - cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found' && false)" silent: true - "go test -count=1 -timeout=15m -v ./test/cli" quality: desc: Run quality tests cmds: - "cd test/quality && make" ## Test-fixture-related targets ################################# fingerprints: desc: Generate test fixture fingerprints generates: - test/integration/testdata/cache.fingerprint - test/install/cache.fingerprint - test/cli/testdata/cache.fingerprint cmds: # for IMAGE integration test fixtures - "cd test/integration/testdata && make cache.fingerprint" # for INSTALL integration test fixtures - "cd test/install && make cache.fingerprint" # for CLI test fixtures - "cd test/cli/testdata && make cache.fingerprint" show-test-image-cache: silent: true cmds: - "echo '\nDocker daemon cache:'" - "docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}:{{`{{.Tag}}`}}' | grep stereoscope-fixture- | sort" - "echo '\nTar cache:'" - 'find . -type f -wholename "**/testdata/snapshot/*" | sort' ## install.sh testing targets ################################# install-test: cmds: - "cd test/install && make" install-test-cache-save: cmds: - "cd test/install && make save" install-test-cache-load: cmds: - "cd test/install && make load" install-test-ci-mac: cmds: - "cd test/install && make ci-test-mac" generate-compare-file: cmd: "go run ./cmd/{{ .PROJECT }} {{ .COMPARE_TEST_IMAGE }} -o json > {{ .COMPARE_DIR }}/testdata/acceptance-{{ .COMPARE_TEST_IMAGE }}.json" compare-mac: deps: [tmpdir] cmd: | {{ .COMPARE_DIR }}/mac.sh \ {{ .SNAPSHOT_DIR }} \ {{ .COMPARE_DIR }} \ {{ .COMPARE_TEST_IMAGE }} \ {{ .TMP_DIR }} compare-linux: cmds: - task: compare-test-deb-package-install - task: compare-test-rpm-package-install compare-test-deb-package-install: deps: [tmpdir] cmd: | {{ .COMPARE_DIR }}/deb.sh \ {{ .SNAPSHOT_DIR }} \ {{ .COMPARE_DIR }} \ {{ .COMPARE_TEST_IMAGE }} \ {{ .TMP_DIR }} compare-test-rpm-package-install: deps: [tmpdir] cmd: | {{ .COMPARE_DIR }}/rpm.sh \ {{ .SNAPSHOT_DIR }} \ {{ .COMPARE_DIR }} \ {{ .COMPARE_TEST_IMAGE }} \ {{ .TMP_DIR }} ## Code and data generation targets ################################# generate: desc: Run code and data generation tasks cmds: - task: generate-json-schema - task: generate-db-schema - task: generate-codename-data generate-json-schema: desc: Generate a new JSON schema cmds: # re-generate package metadata - "cd grype/internal && go generate" # generate the JSON schema for the CLI output - "cd cmd/grype/cli/commands/internal/jsonschema && go run ." generate-db-schema: desc: Generate database blob JSON schemas cmds: # generate the JSON schema for database blob types - "cd grype/db/v6/schema && go run ." generate-codename-data: desc: Generate OS codename lookup data cmds: - "go generate ./grype/db" - task: format ## Build-related targets ################################# build: desc: Build the project deps: [tools, tmpdir] generates: - "{{ .PROJECT }}" cmds: - silent: true cmd: | echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml - "{{ .BUILD_CMD }}" snapshot: desc: Create a snapshot release aliases: - build deps: [tools, tmpdir] sources: - cmd/**/*.go - "{{ .PROJECT }}/**/*.go" - internal/**/*.go method: checksum generates: - "{{ .SNAPSHOT_BIN }}" cmds: - silent: true cmd: | echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml - "{{ .SNAPSHOT_CMD }}" changelog: desc: Generate a changelog deps: [tools] generates: - "{{ .CHANGELOG }}" - "{{ .NEXT_VERSION }}" cmds: - "{{ .TOOL_DIR }}/chronicle -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}" - "{{ .TOOL_DIR }}/glow -w 0 {{ .CHANGELOG }}" ## Release targets ################################# release: desc: Create a release interactive: true deps: [tools] cmds: - cmd: .github/scripts/trigger-release.sh silent: true ## CI-only targets ################################# ci-check: # desc: "[CI only] Are you in CI?" cmds: - cmd: .github/scripts/ci-check.sh silent: true ci-release: # desc: "[CI only] Create a release" deps: [tools] cmds: - task: ci-check - "{{ .TOOL_DIR }}/chronicle -vvv > CHANGELOG.md" - cmd: "cat CHANGELOG.md" silent: true - "{{ .RELEASE_CMD }}" ci-validate-test-config: # desc: "[CI only] Ensure the update URL is not overridden (not pointing to staging)" silent: true cmd: | bash -c '\ grep -q "update-url" test/grype-test-config.yaml; \ if [ $? -eq 0 ]; then \ echo "Found \"update-url\" in CLI testing config. Cannot release if previous CLI testing did not use production (default) values"; \ else echo "Test configuration valid" fi' ## Cleanup targets ################################# clean-snapshot: desc: Remove any snapshot builds cmds: - "rm -rf {{ .SNAPSHOT_DIR }}" - "rm -rf {{ .TMP_DIR }}/goreleaser.yaml" clean-cache: desc: Remove all docker cache and local image tar cache cmds: - 'find . -type f -wholename "**/testdata/cache/stereoscope-fixture-*.tar" -delete' - "docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force" ================================================ FILE: artifacthub-repo.yml ================================================ # See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml repositoryID: 8ff93ef9-2a75-40b6-945e-514e936fe9bb owners: - name: wagoodman email: wagoodman@gmail.com ================================================ FILE: cmd/grype/cli/cli.go ================================================ package cli import ( "errors" "os" "runtime/debug" "strings" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/commands" grypeHandler "github.com/anchore/grype/cmd/grype/cli/ui" "github.com/anchore/grype/cmd/grype/internal/ui" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/redact" "github.com/anchore/stereoscope" syftHandler "github.com/anchore/syft/cmd/syft/cli/ui" "github.com/anchore/syft/syft" ) func Application(id clio.Identification) clio.Application { app, _ := create(id) return app } func Command(id clio.Identification) *cobra.Command { _, cmd := create(id) return cmd } func SetupConfig(id clio.Identification) *clio.SetupConfig { return clio.NewSetupConfig(id). WithGlobalConfigFlag(). // add persistent -c for reading an application config from WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config WithConfigInRootHelp(). // --help on the root command renders the full application config in the help text WithUIConstructor( // select a UI based on the logging configuration and state of stdin (if stdin is a tty) func(cfg clio.Config) (*clio.UICollection, error) { // remove CI var from consideration when determining if we should use the UI lipgloss.SetDefaultRenderer(lipgloss.NewRenderer(os.Stdout, termenv.WithEnvironment(environWithoutCI{}))) // setup the UIs noUI := ui.None(cfg.Log.Quiet) if !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet { return clio.NewUICollection(noUI), nil } return clio.NewUICollection( ui.New(cfg.Log.Quiet, grypeHandler.New(grypeHandler.DefaultHandlerConfig()), syftHandler.New(syftHandler.DefaultHandlerConfig()), ), noUI, ), nil }, ). WithInitializers( func(state *clio.State) error { // clio is setting up and providing the bus, redact store, and logger to the application. Once loaded, // we can hoist them into the internal packages for global use. stereoscope.SetBus(state.Bus) syft.SetBus(state.Bus) bus.Set(state.Bus) redact.Set(state.RedactStore) log.Set(state.Logger) syft.SetLogger(state.Logger.Nested("from", "syft")) stereoscope.SetLogger(state.Logger.Nested("from", "stereoscope")) return nil }, ). WithPostRuns(func(_ *clio.State, _ error) { stereoscope.Cleanup() //nolint:staticcheck }). WithMapExitCode(func(err error) int { // return exit code 2 to indicate when a vulnerability severity is discovered // that is equal or above the given --fail-on severity value. if errors.Is(err, grypeerr.ErrAboveSeverityThreshold) { return 2 } // return exit code 100 to indicate a DB upgrade is available (cmd: db check). if errors.Is(err, grypeerr.ErrDBUpgradeAvailable) { return 100 } return 1 }) } func create(id clio.Identification) (clio.Application, *cobra.Command) { clioCfg := SetupConfig(id) app := clio.New(*clioCfg) rootCmd := commands.Root(app) // add sub-commands rootCmd.AddCommand( commands.DB(app), commands.Completion(app), commands.Explain(app), clio.VersionCommand(id, syftVersion, dbVersion), clio.ConfigCommand(app, nil), ) return app, rootCmd } func syftVersion() (string, any) { buildInfo, ok := debug.ReadBuildInfo() if !ok { log.Debug("unable to find the buildinfo section of the binary (syft version is unknown)") return "", "" } for _, d := range buildInfo.Deps { if d.Path == "github.com/anchore/syft" { return "Syft Version", d.Version } } log.Debug("unable to find 'github.com/anchore/syft' from the buildinfo section of the binary") return "", "" } func dbVersion() (string, any) { return "Supported DB Schema", v6.ModelVersion } type environWithoutCI struct { } func (e environWithoutCI) Environ() []string { var out []string for _, s := range os.Environ() { if strings.HasPrefix(s, "CI=") { continue } out = append(out, s) } return out } func (e environWithoutCI) Getenv(s string) string { if s == "CI" { return "" } return os.Getenv(s) } ================================================ FILE: cmd/grype/cli/cli_test.go ================================================ package cli import ( "testing" "github.com/stretchr/testify/require" "github.com/anchore/clio" ) func Test_Command(t *testing.T) { root := Command(clio.Identification{ Name: "test-name", Version: "test-version", }) require.Equal(t, root.Name(), "test-name") require.NotEmpty(t, root.Commands()) } ================================================ FILE: cmd/grype/cli/commands/completion.go ================================================ package commands import ( "context" "os" "strings" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/spf13/cobra" "github.com/anchore/clio" ) // Completion returns a command to provide completion to various terminal shells func Completion(app clio.Application) *cobra.Command { return &cobra.Command{ Use: "completion [bash|zsh|fish]", Short: "Generate a shell completion for Grype (listing local docker images)", Long: `To load completions (docker image list): Bash: $ source <(grype completion bash) # To load completions for each session, execute once: Linux: $ grype completion bash > /etc/bash_completion.d/grype MacOS: $ grype completion bash > /usr/local/etc/bash_completion.d/grype Zsh: # If shell completion is not already enabled in your environment you will need # to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ grype completion zsh > "${fpath[1]}/_grype" # You will need to start a new shell for this setup to take effect. Fish: $ grype completion fish | source # To load completions for each session, execute once: $ grype completion fish > ~/.config/fish/completions/grype.fish `, DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "fish", "zsh"}, PreRunE: disableUI(app), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), RunE: func(cmd *cobra.Command, args []string) error { var err error switch args[0] { case "zsh": err = cmd.Root().GenZshCompletion(os.Stdout) case "bash": err = cmd.Root().GenBashCompletion(os.Stdout) case "fish": err = cmd.Root().GenFishCompletion(os.Stdout, true) } return err }, } } func listLocalDockerImages(prefix string) ([]string, error) { var repoTags = make([]string, 0) ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return repoTags, err } // Only want to return tagged images imageListArgs := filters.NewArgs() imageListArgs.Add("dangling", "false") images, err := cli.ImageList(ctx, image.ListOptions{All: false, Filters: imageListArgs}) if err != nil { return repoTags, err } for _, image := range images { // image may have multiple tags for _, tag := range image.RepoTags { if strings.HasPrefix(tag, prefix) { repoTags = append(repoTags, tag) } } } return repoTags, nil } func dockerImageValidArgsFunction(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided dockerImageRepoTags, err := listLocalDockerImages(toComplete) if err != nil { // Indicates that an error occurred and completions should be ignored return []string{"completion failed"}, cobra.ShellCompDirectiveError } if len(dockerImageRepoTags) == 0 { return []string{"no docker images found"}, cobra.ShellCompDirectiveError } // ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have // been provided (without implying other possible directives) return dockerImageRepoTags, cobra.ShellCompDirectiveDefault } ================================================ FILE: cmd/grype/cli/commands/db.go ================================================ package commands import ( "github.com/spf13/cobra" "github.com/anchore/clio" ) const ( jsonOutputFormat = "json" tableOutputFormat = "table" textOutputFormat = "text" ) func DB(app clio.Application) *cobra.Command { db := &cobra.Command{ Use: "db", Short: "vulnerability database operations", } db.AddCommand( DBCheck(app), DBDelete(app), DBImport(app), DBList(app), DBStatus(app), DBUpdate(app), DBSearch(app), DBProviders(app), ) return db } ================================================ FILE: cmd/grype/cli/commands/db_check.go ================================================ package commands import ( "encoding/json" "fmt" "io" "os" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/internal/log" ) type dbCheckOptions struct { Output string `yaml:"output" json:"output" mapstructure:"output"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } var _ clio.FlagAdder = (*dbCheckOptions)(nil) func (d *dbCheckOptions) AddFlags(flags clio.FlagSet) { flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, json])") } func DBCheck(app clio.Application) *cobra.Command { opts := &dbCheckOptions{ Output: textOutputFormat, DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()), } cmd := &cobra.Command{ Use: "check", Short: "Check to see if there is a database update available", PreRunE: func(cmd *cobra.Command, args []string) error { // DB commands should not opt into the low-pass check filter opts.DB.MaxUpdateCheckFrequency = 0 return disableUI(app)(cmd, args) }, Args: cobra.ExactArgs(0), RunE: func(_ *cobra.Command, _ []string) error { return runDBCheck(*opts) }, } // prevent from being shown in the grype config type configWrapper struct { Hidden *dbCheckOptions `json:"-" yaml:"-" mapstructure:"-"` *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand}) } func runDBCheck(opts dbCheckOptions) error { client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } cfg := opts.ToCuratorConfig() current, err := db.ReadDescription(cfg.DBFilePath()) if err != nil { log.WithFields("error", err).Debug("unable to read current database metadata") current = nil } archive, err := client.IsUpdateAvailable(current) if err != nil { return fmt.Errorf("unable to check for vulnerability database update: %w", err) } updateAvailable := archive != nil if err := presentNewDBCheck(opts.Output, os.Stdout, updateAvailable, current, archive); err != nil { return err } if updateAvailable { return grypeerr.ErrDBUpgradeAvailable } return nil } type dbCheckJSON struct { CurrentDB *db.Description `json:"currentDB"` CandidateDB *distribution.Archive `json:"candidateDB"` UpdateAvailable bool `json:"updateAvailable"` } func presentNewDBCheck(format string, writer io.Writer, updateAvailable bool, current *db.Description, candidate *distribution.Archive) error { switch format { case textOutputFormat: if current != nil { fmt.Fprintf(writer, "Installed DB version %s was built on %s\n", current.SchemaVersion, current.Built.String()) } else { fmt.Fprintln(writer, "No installed DB version found") } if !updateAvailable { fmt.Fprintln(writer, "No update available") return nil } fmt.Fprintf(writer, "Updated DB version %s was built on %s\n", candidate.SchemaVersion, candidate.Built.String()) fmt.Fprintln(writer, "You can run 'grype db update' to update to the latest db") case jsonOutputFormat: data := dbCheckJSON{ CurrentDB: current, CandidateDB: candidate, UpdateAvailable: updateAvailable, } enc := json.NewEncoder(writer) enc.SetEscapeHTML(false) enc.SetIndent("", " ") if err := enc.Encode(&data); err != nil { return fmt.Errorf("failed to db listing information: %+v", err) } default: return fmt.Errorf("unsupported output format: %s", format) } return nil } ================================================ FILE: cmd/grype/cli/commands/db_check_test.go ================================================ package commands import ( "bytes" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/internal/schemaver" ) func TestPresentNewDBCheck(t *testing.T) { currentDB := &db.Description{ SchemaVersion: schemaver.New(6, 0, 0), Built: db.Time{Time: time.Date(2023, 11, 25, 12, 0, 0, 0, time.UTC)}, } candidateDB := &distribution.Archive{ Description: db.Description{ SchemaVersion: schemaver.New(6, 0, 1), Built: db.Time{Time: time.Date(2023, 11, 26, 12, 0, 0, 0, time.UTC)}, }, Path: "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz", Checksum: "sha256:1234561234567890345674561234567890345678", } tests := []struct { name string format string updateAvailable bool current *db.Description candidate *distribution.Archive expectedText string expectErr require.ErrorAssertionFunc }{ { name: "text format with update available", format: textOutputFormat, updateAvailable: true, current: currentDB, candidate: candidateDB, expectedText: ` Installed DB version v6.0.0 was built on 2023-11-25T12:00:00Z Updated DB version v6.0.1 was built on 2023-11-26T12:00:00Z You can run 'grype db update' to update to the latest db `, }, { name: "text format without update available", format: textOutputFormat, updateAvailable: false, current: currentDB, candidate: nil, expectedText: ` Installed DB version v6.0.0 was built on 2023-11-25T12:00:00Z No update available `, }, { name: "json format with update available", format: jsonOutputFormat, updateAvailable: true, current: currentDB, candidate: candidateDB, expectedText: ` { "currentDB": { "schemaVersion": "v6.0.0", "built": "2023-11-25T12:00:00Z" }, "candidateDB": { "schemaVersion": "v6.0.1", "built": "2023-11-26T12:00:00Z", "path": "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz", "checksum": "sha256:1234561234567890345674561234567890345678" }, "updateAvailable": true } `, }, { name: "json format without update available", format: jsonOutputFormat, updateAvailable: false, current: currentDB, candidate: nil, expectedText: ` { "currentDB": { "schemaVersion": "v6.0.0", "built": "2023-11-25T12:00:00Z" }, "candidateDB": null, "updateAvailable": false } `, }, { name: "unsupported format", format: "xml", expectErr: requireErrorContains("unsupported output format: xml"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectErr == nil { tt.expectErr = require.NoError } buf := &bytes.Buffer{} err := presentNewDBCheck(tt.format, buf, tt.updateAvailable, tt.current, tt.candidate) tt.expectErr(t, err) if err != nil { return } assert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(buf.String())) }) } } func requireErrorContains(expected string) require.ErrorAssertionFunc { return func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.Error(t, err) assert.Contains(t, err.Error(), expected) } } ================================================ FILE: cmd/grype/cli/commands/db_delete.go ================================================ package commands import ( "fmt" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" ) func DBDelete(app clio.Application) *cobra.Command { opts := options.DefaultDatabaseCommand(app.ID()) cmd := &cobra.Command{ Use: "delete", Short: "Delete the vulnerability database", Args: cobra.ExactArgs(0), PreRunE: disableUI(app), RunE: func(_ *cobra.Command, _ []string) error { return runDBDelete(*opts) }, } // prevent from being shown in the grype config type configWrapper struct { *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{opts}) } func runDBDelete(opts options.DatabaseCommand) error { client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } c, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create curator: %w", err) } if err := c.Delete(); err != nil { return fmt.Errorf("unable to delete vulnerability database: %+v", err) } return stderrPrintLnf("Vulnerability database deleted") } ================================================ FILE: cmd/grype/cli/commands/db_import.go ================================================ package commands import ( "fmt" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal/log" ) func DBImport(app clio.Application) *cobra.Command { opts := options.DefaultDatabaseCommand(app.ID()) cmd := &cobra.Command{ Use: "import FILE | URL", Short: "Import a vulnerability database or archive from a local file or URL", Long: fmt.Sprintf("import a vulnerability database archive from a local FILE or URL.\nDB archives can be obtained from %q (or running `db list`). If the URL has a `checksum` query parameter with a fully qualified digest (e.g. 'sha256:abc728...') then the archive/DB will be verified against this value.", opts.DB.UpdateURL), Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { return runDBImport(*opts, args[0]) }, } // prevent from being shown in the grype config type configWrapper struct { *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{opts}) } func runDBImport(opts options.DatabaseCommand, reference string) error { // TODO: tui update? better logging? client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } c, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create curator: %w", err) } log.WithFields("reference", reference).Infof("importing vulnerability database archive") if err := c.Import(reference); err != nil { return fmt.Errorf("unable to import vulnerability database: %w", err) } s := c.Status() log.WithFields("built", s.Built.String(), "status", renderStoreValidation(s)).Info("vulnerability database imported") return nil } ================================================ FILE: cmd/grype/cli/commands/db_list.go ================================================ package commands import ( "encoding/json" "fmt" "io" "net/url" "os" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/db/v6/distribution" ) type dbListOptions struct { Output string `yaml:"output" json:"output" mapstructure:"output"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } var _ clio.FlagAdder = (*dbListOptions)(nil) func (d *dbListOptions) AddFlags(flags clio.FlagSet) { flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, raw, json])") } func DBList(app clio.Application) *cobra.Command { opts := &dbListOptions{ Output: textOutputFormat, DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()), } cmd := &cobra.Command{ Use: "list", Short: "List all DBs available according to the listing URL", PreRunE: disableUI(app), Args: cobra.ExactArgs(0), RunE: func(_ *cobra.Command, _ []string) error { return runDBList(*opts) }, } // prevent from being shown in the grype config type configWrapper struct { Hidden *dbListOptions `json:"-" yaml:"-" mapstructure:"-"` *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand}) } func runDBList(opts dbListOptions) error { c, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } latest, err := c.Latest() if err != nil { return fmt.Errorf("unable to get database listing: %w", err) } u, err := c.ResolveArchiveURL(latest.Archive) if err != nil { return fmt.Errorf("unable to resolve database URL: %w", err) } return presentDBList(opts.Output, u, opts.DB.UpdateURL, os.Stdout, latest) } func presentDBList(format string, archiveURL, listingURL string, writer io.Writer, latest *distribution.LatestDocument) error { if latest == nil { return fmt.Errorf("no database listing found") } // remove query params archiveURLObj, err := url.Parse(archiveURL) if err != nil { return fmt.Errorf("unable to parse db URL %q: %w", archiveURL, err) } archiveURLObj.RawQuery = "" if listingURL == distribution.DefaultConfig().LatestURL { // append on the schema listingURL = fmt.Sprintf("%s/v%v/%s", listingURL, latest.SchemaVersion.Model, distribution.LatestFileName) } switch format { case textOutputFormat: fmt.Fprintf(writer, "Status: %s\n", latest.Status) fmt.Fprintf(writer, "Schema: %s\n", latest.SchemaVersion.String()) fmt.Fprintf(writer, "Built: %s\n", latest.Built.String()) fmt.Fprintf(writer, "Listing: %s\n", listingURL) fmt.Fprintf(writer, "DB URL: %s\n", archiveURLObj.String()) fmt.Fprintf(writer, "Checksum: %s\n", latest.Checksum) case jsonOutputFormat, "raw": enc := json.NewEncoder(writer) enc.SetEscapeHTML(false) enc.SetIndent("", " ") // why make an array? We are reserving the right to list additional entries in the future without the // need to change from an object to an array at that point in time. This will be useful if we implement // the history.json functionality for grabbing historical database listings. if err := enc.Encode([]any{latest}); err != nil { return fmt.Errorf("failed to db listing information: %+v", err) } default: return fmt.Errorf("unsupported output format: %s", format) } return nil } ================================================ FILE: cmd/grype/cli/commands/db_list_test.go ================================================ package commands import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/internal/schemaver" ) func Test_ListingUserAgent(t *testing.T) { t.Run("new", func(t *testing.T) { listingFile := "/latest.json" got := "" // setup mock handler := http.NewServeMux() handler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) { got = r.Header.Get("User-Agent") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(&distribution.LatestDocument{ Status: "active", Archive: distribution.Archive{ Description: db.Description{ SchemaVersion: schemaver.New(6, 0, 0), Built: db.Time{Time: time.Now()}, }, Path: "vulnerability-db_v6.0.0.tar.gz", Checksum: "sha256:dummychecksum", }, }) }) mockSrv := httptest.NewServer(handler) defer mockSrv.Close() dbOptions := *options.DefaultDatabaseCommand(clio.Identification{ Name: "new-app", Version: "v4.0.0", }) dbOptions.DB.RequireUpdateCheck = true dbOptions.DB.UpdateURL = mockSrv.URL + listingFile err := runDBList(dbListOptions{ Output: textOutputFormat, DatabaseCommand: dbOptions, }) require.NoError(t, err) if got != "new-app v4.0.0" { t.Errorf("expected User-Agent header to match, got: %v", got) } }) } func TestPresentDBList(t *testing.T) { latestDoc := &distribution.LatestDocument{ Status: "active", Archive: distribution.Archive{ Description: db.Description{ SchemaVersion: schemaver.New(6, 0, 0), Built: db.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)}, }, Path: "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst", Checksum: "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4", }, } tests := []struct { name string format string baseURL string archiveURL string latest *distribution.LatestDocument expectedText string expectedErr require.ErrorAssertionFunc }{ { name: "valid text format", format: textOutputFormat, latest: latestDoc, baseURL: "http://localhost:8000/latest.json", archiveURL: "http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst", expectedText: `Status: active Schema: v6.0.0 Built: 2024-11-27T14:43:17Z Listing: http://localhost:8000/latest.json DB URL: http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4 `, expectedErr: require.NoError, }, { name: "complete default values", format: textOutputFormat, latest: latestDoc, baseURL: "https://grype.anchore.io/databases", archiveURL: "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst", expectedText: `Status: active Schema: v6.0.0 Built: 2024-11-27T14:43:17Z Listing: https://grype.anchore.io/databases/v6/latest.json DB URL: https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4 `, expectedErr: require.NoError, }, { name: "valid JSON format", format: jsonOutputFormat, latest: latestDoc, expectedText: `[ { "status": "active", "schemaVersion": "v6.0.0", "built": "2024-11-27T14:43:17Z", "path": "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst", "checksum": "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4" } ] `, expectedErr: require.NoError, }, { name: "nil latest document", format: textOutputFormat, latest: nil, expectedErr: requireErrorContains("no database listing found"), }, { name: "unsupported format", format: "unsupported", latest: latestDoc, expectedErr: requireErrorContains("unsupported output format"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { writer := &bytes.Buffer{} err := presentDBList(tt.format, tt.archiveURL, tt.baseURL, writer, tt.latest) if tt.expectedErr == nil { tt.expectedErr = require.NoError } tt.expectedErr(t, err) if err != nil { return } require.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String())) }) } } ================================================ FILE: cmd/grype/cli/commands/db_providers.go ================================================ package commands import ( "encoding/json" "fmt" "io" "strings" "time" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal/bus" ) type dbProvidersOptions struct { Output string `yaml:"output" json:"output"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } var _ clio.FlagAdder = (*dbProvidersOptions)(nil) func (d *dbProvidersOptions) AddFlags(flags clio.FlagSet) { flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[table, json])") } func DBProviders(app clio.Application) *cobra.Command { opts := &dbProvidersOptions{ Output: tableOutputFormat, DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()), } cmd := &cobra.Command{ Use: "providers", Short: "List vulnerability providers that are in the database", Args: cobra.ExactArgs(0), RunE: func(_ *cobra.Command, _ []string) error { return runDBProviders(opts) }, } // prevent from being shown in the grype config type configWrapper struct { Hidden *dbProvidersOptions `json:"-" yaml:"-" mapstructure:"-"` *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand}) } func runDBProviders(opts *dbProvidersOptions) error { client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } c, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create curator: %w", err) } reader, err := c.Reader() if err != nil { return fmt.Errorf("unable to get providers: %w", err) } providerModels, err := reader.AllProviders() if err != nil { return fmt.Errorf("unable to get providers: %w", err) } sb := &strings.Builder{} switch opts.Output { case tableOutputFormat, textOutputFormat: err = displayDBProvidersTable(toProviders(providerModels), sb) if err != nil { return err } case jsonOutputFormat: err = displayDBProvidersJSON(toProviders(providerModels), sb) if err != nil { return err } default: return fmt.Errorf("unsupported output format: %s", opts.Output) } bus.Report(sb.String()) return nil } type provider struct { Name string `json:"name"` Version string `json:"version"` Processor string `json:"processor"` DateCaptured *time.Time `json:"dateCaptured"` InputDigest string `json:"inputDigest"` } func toProviders(providers []v6.Provider) []provider { var res []provider for _, p := range providers { res = append(res, provider{ Name: p.ID, Version: p.Version, Processor: p.Processor, DateCaptured: p.DateCaptured, InputDigest: p.InputDigest, }) } return res } func displayDBProvidersTable(providers []provider, output io.Writer) error { rows := [][]string{} for _, p := range providers { rows = append(rows, []string{p.Name, p.Version, p.Processor, p.DateCaptured.String(), p.InputDigest}) } table := newTable(output, []string{"Name", "Version", "Processor", "Date Captured", "Input Digest"}) if err := table.Bulk(rows); err != nil { return fmt.Errorf("failed to add table rows: %w", err) } return table.Render() } func displayDBProvidersJSON(providers []provider, output io.Writer) error { encoder := json.NewEncoder(output) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") err := encoder.Encode(providers) if err != nil { return fmt.Errorf("cannot display json: %w", err) } return nil } ================================================ FILE: cmd/grype/cli/commands/db_providers_test.go ================================================ package commands import ( "bytes" "testing" "time" "github.com/stretchr/testify/require" ) func TestDisplayDBProvidersTable(t *testing.T) { providers := []provider{ { Name: "provider1", Version: "1.0.0", Processor: "vunnel@3.2", DateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)), InputDigest: "xxh64:1234567834567", }, { Name: "provider2", Version: "2.0.0", Processor: "vunnel@3.2", DateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)), InputDigest: "xxh64:9876543212345", }, } expectedOutput := `NAME VERSION PROCESSOR DATE CAPTURED INPUT DIGEST provider1 1.0.0 vunnel@3.2 2024-11-25 14:30:00 +0000 UTC xxh64:1234567834567 provider2 2.0.0 vunnel@3.2 2024-11-26 10:15:00 +0000 UTC xxh64:9876543212345 ` var output bytes.Buffer require.NoError(t, displayDBProvidersTable(providers, &output)) require.Equal(t, expectedOutput, output.String()) } func TestDisplayDBProvidersJSON(t *testing.T) { providers := []provider{ { Name: "provider1", Version: "1.0.0", Processor: "vunnel@3.2", DateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)), InputDigest: "xxh64:1234567834567", }, { Name: "provider2", Version: "2.0.0", Processor: "vunnel@3.2", DateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)), InputDigest: "xxh64:9876543212345", }, } expectedJSON := `[ { "name": "provider1", "version": "1.0.0", "processor": "vunnel@3.2", "dateCaptured": "2024-11-25T14:30:00Z", "inputDigest": "xxh64:1234567834567" }, { "name": "provider2", "version": "2.0.0", "processor": "vunnel@3.2", "dateCaptured": "2024-11-26T10:15:00Z", "inputDigest": "xxh64:9876543212345" } ] ` var output bytes.Buffer err := displayDBProvidersJSON(providers, &output) require.NoError(t, err) require.JSONEq(t, expectedJSON, output.String()) } func timeRef(t time.Time) *time.Time { return &t } ================================================ FILE: cmd/grype/cli/commands/db_search.go ================================================ package commands import ( "encoding/json" "errors" "fmt" "io" "regexp" "sort" "strings" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" "github.com/anchore/grype/cmd/grype/cli/options" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" ) type dbSearchMatchOptions struct { Format options.DBSearchFormat `yaml:",inline" mapstructure:",squash"` Vulnerability options.DBSearchVulnerabilities `yaml:",inline" mapstructure:",squash"` Package options.DBSearchPackages `yaml:",inline" mapstructure:",squash"` OS options.DBSearchOSs `yaml:",inline" mapstructure:",squash"` Bounds options.DBSearchBounds `yaml:",inline" mapstructure:",squash"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } var alasPattern = regexp.MustCompile(`^alas[\w]*-\d+-\d+$`) func (o *dbSearchMatchOptions) applyArgs(args []string) error { for _, arg := range args { lowerArg := strings.ToLower(arg) switch { case hasAnyPrefix(lowerArg, "cpe:", "purl:"): // this is explicitly a package... log.WithFields("value", arg).Trace("assuming arg is a package specifier") o.Package.Packages = append(o.Package.Packages, arg) case hasAnyPrefix(lowerArg, "cve-", "ghsa-", "elsa-", "rhsa-") || alasPattern.MatchString(lowerArg): // this is a vulnerability... log.WithFields("value", arg).Trace("assuming arg is a vulnerability ID") o.Vulnerability.VulnerabilityIDs = append(o.Vulnerability.VulnerabilityIDs, arg) default: // assume this is a package name log.WithFields("value", arg).Trace("assuming arg is a package name") o.Package.Packages = append(o.Package.Packages, arg) } } if err := o.Vulnerability.PostLoad(); err != nil { return err } if err := o.Package.PostLoad(); err != nil { return err } return nil } func hasAnyPrefix(s string, prefixes ...string) bool { for _, prefix := range prefixes { if strings.HasPrefix(s, prefix) { return true } } return false } func DBSearch(app clio.Application) *cobra.Command { opts := &dbSearchMatchOptions{ Format: options.DefaultDBSearchFormat(), Vulnerability: options.DBSearchVulnerabilities{ UseVulnIDFlag: true, }, Bounds: options.DefaultDBSearchBounds(), DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()), } cmd := &cobra.Command{ Use: "search", Short: "Search the DB for vulnerabilities or affected packages", Example: ` Search for affected packages by vulnerability ID: $ grype db search --vuln ELSA-2023-12205 Search for affected packages by package name: $ grype db search --pkg log4j Search for affected packages by package name, filtering down to a specific vulnerability: $ grype db search --pkg log4j --vuln CVE-2021-44228 Search for affected packages by PURL (note: version is not considered): $ grype db search --pkg 'pkg:rpm/redhat/openssl' # or: '--ecosystem rpm --pkg openssl Search for affected packages by CPE (note: version/update is not considered): $ grype db search --pkg 'cpe:2.3:a:jetty:jetty_http_server:*:*:*:*:*:*:*:*' $ grype db search --pkg 'cpe:/a:jetty:jetty_http_server'`, PreRunE: disableUI(app), RunE: func(cmd *cobra.Command, args []string) (err error) { if len(args) > 0 { // try to stay backwards compatible with v5 search command (which takes args) if err := opts.applyArgs(args); err != nil { return err } } err = runDBSearchMatches(*opts) if err != nil { if errors.Is(err, dbsearch.ErrNoSearchCriteria) { _ = cmd.Usage() } return err } return nil }, } cmd.AddCommand( DBSearchVulnerabilities(app), ) // prevent from being shown in the grype config type configWrapper struct { Hidden *dbSearchMatchOptions `json:"-" yaml:"-" mapstructure:"-"` *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand}) } func runDBSearchMatches(opts dbSearchMatchOptions) error { client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } curator, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create curator: %w", err) } reader, err := curator.Reader() if err != nil { return fmt.Errorf("unable to get providers: %w", err) } if err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil { return err } rows, queryErr := dbsearch.FindMatches(reader, dbsearch.AffectedPackagesOptions{ Vulnerability: opts.Vulnerability.Specs, Package: opts.Package.PkgSpecs, CPE: opts.Package.CPESpecs, OS: opts.OS.Specs, AllowBroadCPEMatching: opts.Package.AllowBroadCPEMatching, RecordLimit: opts.Bounds.RecordLimit, FixedStates: opts.Vulnerability.FixedState, }) if queryErr != nil { if !errors.Is(queryErr, v6.ErrLimitReached) { return queryErr } } sb := &strings.Builder{} err = presentDBSearchMatches(opts.Format.Output, rows, sb) rep := sb.String() if rep != "" { bus.Report(rep) } if err != nil { return fmt.Errorf("unable to present search results: %w", err) } return queryErr } func presentDBSearchMatches(outputFormat string, structuredRows dbsearch.Matches, output io.Writer) error { switch outputFormat { case tableOutputFormat: if len(structuredRows) == 0 { bus.Notify("No results found") return nil } rows := renderDBSearchPackagesTableRows(structuredRows.Flatten()) table := newTable(output, []string{"Vulnerability", "Package", "Ecosystem", "Namespace", "Version Constraint"}) if err := table.Bulk(rows); err != nil { return fmt.Errorf("failed to add table rows: %+v", err) } return table.Render() case jsonOutputFormat: if structuredRows == nil { // always allocate the top level collection structuredRows = dbsearch.Matches{} } enc := json.NewEncoder(output) enc.SetEscapeHTML(false) enc.SetIndent("", " ") if err := enc.Encode(structuredRows); err != nil { return fmt.Errorf("failed to encode diff information: %+v", err) } default: return fmt.Errorf("unsupported output format: %s", outputFormat) } return nil } func renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackage) [][]string { var rows [][]string for _, rr := range structuredRows { var pkgOrCPE, ecosystem string if rr.Package != nil { pkgOrCPE = rr.Package.Name ecosystem = rr.Package.Ecosystem } else if rr.CPE != nil { pkgOrCPE = rr.CPE.String() ecosystem = rr.CPE.TargetSoftware } var ranges []string for _, ra := range rr.Detail.Ranges { ranges = append(ranges, ra.Version.Constraint) } rangeStr := strings.Join(ranges, " || ") rows = append(rows, []string{rr.Vulnerability.ID, pkgOrCPE, ecosystem, mimicV5Namespace(rr), rangeStr}) } // sort rows by each column sort.Slice(rows, func(i, j int) bool { for k := range rows[i] { if rows[i][k] != rows[j][k] { return rows[i][k] < rows[j][k] } } return false }) return rows } func mimicV5Namespace(row dbsearch.AffectedPackage) string { namespace := v6.MimicV5Namespace(&row.Vulnerability.Model, row.Model) if row.Model != nil && row.Model.OperatingSystem != nil && row.Model.OperatingSystem.Channel != "" { return namespace + ":" + row.Model.OperatingSystem.Channel } return namespace } ================================================ FILE: cmd/grype/cli/commands/db_search_test.go ================================================ package commands import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" "github.com/anchore/grype/cmd/grype/cli/options" v6 "github.com/anchore/grype/grype/db/v6" ) func TestDBSearchMatchOptionsApplyArgs(t *testing.T) { testCases := []struct { name string args []string expectedPackages []string expectedVulnIDs []string expectedErrMessage string }{ { name: "empty arguments", args: []string{}, expectedPackages: []string{}, expectedVulnIDs: []string{}, }, { name: "valid cpe", args: []string{"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*"}, expectedPackages: []string{ "cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*", }, expectedVulnIDs: []string{}, }, { name: "valid purl", args: []string{"pkg:npm/package-name@1.0.0"}, expectedPackages: []string{ "pkg:npm/package-name@1.0.0", }, expectedVulnIDs: []string{}, }, { name: "valid vulnerability IDs", args: []string{"CVE-2023-0001", "GHSA-1234", "ALAS-2023-1234"}, expectedPackages: []string{}, expectedVulnIDs: []string{ "CVE-2023-0001", "GHSA-1234", "ALAS-2023-1234", }, }, { name: "mixed package and vulns", args: []string{"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*", "CVE-2023-0001"}, expectedPackages: []string{ "cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*", }, expectedVulnIDs: []string{ "CVE-2023-0001", }, }, { name: "plain package name", args: []string{"package-name"}, expectedPackages: []string{ "package-name", }, expectedVulnIDs: []string{}, }, { name: "invalid PostLoad error for Package", args: []string{"pkg:npm/package-name@1.0.0", "cpe:invalid"}, expectedPackages: []string{ "pkg:npm/package-name@1.0.0", }, expectedErrMessage: "invalid CPE", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { opts := &dbSearchMatchOptions{ Vulnerability: options.DBSearchVulnerabilities{}, Package: options.DBSearchPackages{}, } err := opts.applyArgs(tc.args) if tc.expectedErrMessage != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.expectedErrMessage) return } require.NoError(t, err) if d := cmp.Diff(tc.expectedPackages, opts.Package.Packages, cmpopts.EquateEmpty()); d != "" { t.Errorf("unexpected package specifiers: %s", d) } if d := cmp.Diff(tc.expectedVulnIDs, opts.Vulnerability.VulnerabilityIDs, cmpopts.EquateEmpty()); d != "" { t.Errorf("unexpected vulnerability specifiers: %s", d) } }) } } func TestMimicV5Namespace(t *testing.T) { tests := []struct { name string osName string osChannel string expected string }{ { name: "distro namespace without channel", osName: "redhat", osChannel: "", expected: "redhat:distro:redhat:8", }, { name: "distro namespace with channel", osName: "redhat", osChannel: "eus", expected: "redhat:distro:redhat:8:eus", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { row := dbsearch.AffectedPackage{ Vulnerability: dbsearch.VulnerabilityInfo{ Model: v6.VulnerabilityHandle{ Provider: &v6.Provider{ID: "rhel"}, }, }, AffectedPackageInfo: dbsearch.AffectedPackageInfo{ Model: &v6.AffectedPackageHandle{ OperatingSystem: &v6.OperatingSystem{ Name: tt.osName, MajorVersion: "8", MinorVersion: "6", Channel: tt.osChannel, }, Package: &v6.Package{ Name: "test-package", Ecosystem: "rpm", }, }, }, } result := mimicV5Namespace(row) require.Equal(t, tt.expected, result) }) } } ================================================ FILE: cmd/grype/cli/commands/db_search_vuln.go ================================================ package commands import ( "encoding/json" "fmt" "io" "sort" "strings" "time" "github.com/hashicorp/go-multierror" "github.com/scylladb/go-set/strset" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" "github.com/anchore/grype/cmd/grype/cli/options" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal/bus" ) type dbSearchVulnerabilityOptions struct { Format options.DBSearchFormat `yaml:",inline" mapstructure:",squash"` Vulnerability options.DBSearchVulnerabilities `yaml:",inline" mapstructure:",squash"` Bounds options.DBSearchBounds `yaml:",inline" mapstructure:",squash"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } func DBSearchVulnerabilities(app clio.Application) *cobra.Command { opts := &dbSearchVulnerabilityOptions{ Format: options.DefaultDBSearchFormat(), Vulnerability: options.DBSearchVulnerabilities{ UseVulnIDFlag: false, // we input this through the args }, Bounds: options.DefaultDBSearchBounds(), DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()), } cmd := &cobra.Command{ Use: "vuln ID...", Aliases: []string{"vulnerability", "vulnerabilities", "vulns"}, Short: "Search for vulnerabilities within the DB (supports DB schema v6+ only)", Args: func(_ *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("must specify at least one vulnerability ID") } opts.Vulnerability.VulnerabilityIDs = args return nil }, RunE: func(_ *cobra.Command, _ []string) (err error) { return runDBSearchVulnerabilities(*opts) }, } // prevent from being shown in the grype config type configWrapper struct { Hidden *dbSearchVulnerabilityOptions `json:"-" yaml:"-" mapstructure:"-"` *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand}) } func runDBSearchVulnerabilities(opts dbSearchVulnerabilityOptions) error { client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } c, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create curator: %w", err) } reader, err := c.Reader() if err != nil { return fmt.Errorf("unable to get providers: %w", err) } if err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil { return err } rows, err := dbsearch.FindVulnerabilities(reader, dbsearch.VulnerabilitiesOptions{ Vulnerability: opts.Vulnerability.Specs, RecordLimit: opts.Bounds.RecordLimit, }) if err != nil { return err } sb := &strings.Builder{} err = presentDBSearchVulnerabilities(opts.Format.Output, rows, sb) rep := sb.String() if rep != "" { bus.Report(rep) } return err } func validateProvidersFilter(reader v6.Reader, providers []string) error { if len(providers) == 0 { return nil } availableProviders, err := reader.AllProviders() if err != nil { return fmt.Errorf("unable to get providers: %w", err) } activeProviders := strset.New() for _, p := range availableProviders { activeProviders.Add(p.ID) } provSet := strset.New(providers...) diff := strset.Difference(provSet, activeProviders) diffList := diff.List() sort.Strings(diffList) var errs error for _, p := range diffList { errs = multierror.Append(errs, fmt.Errorf("provider not found: %q", p)) } return errs } func presentDBSearchVulnerabilities(outputFormat string, structuredRows []dbsearch.Vulnerability, output io.Writer) error { switch outputFormat { case tableOutputFormat: if len(structuredRows) == 0 { bus.Notify("No results found") return nil } rows := renderDBSearchVulnerabilitiesTableRows(structuredRows) table := newTable(output, []string{"ID", "Provider", "Published", "Severity", "Reference"}) if err := table.Bulk(rows); err != nil { return fmt.Errorf("failed to add table rows: %+v", err) } return table.Render() case jsonOutputFormat: if structuredRows == nil { // always allocate the top level collection structuredRows = []dbsearch.Vulnerability{} } enc := json.NewEncoder(output) enc.SetEscapeHTML(false) enc.SetIndent("", " ") if err := enc.Encode(structuredRows); err != nil { return fmt.Errorf("failed to encode diff information: %+v", err) } default: return fmt.Errorf("unsupported output format: %s", outputFormat) } return nil } func renderDBSearchVulnerabilitiesTableRows(structuredRows []dbsearch.Vulnerability) [][]string { type row struct { Vuln string ProviderWithoutVersions string PublishedDate string Severity string Reference string } versionsByRow := make(map[row][]string) for _, rr := range structuredRows { r := row{ Vuln: rr.ID, ProviderWithoutVersions: rr.Provider, PublishedDate: getDate(rr.PublishedDate), Severity: rr.Severity, Reference: getPrimaryReference(rr.References), } versionsByRow[r] = append(versionsByRow[r], getOSVersions(rr.OperatingSystems)...) } var rows [][]string for r, versions := range versionsByRow { prov := r.ProviderWithoutVersions if len(versions) > 0 { sort.Strings(versions) prov = fmt.Sprintf("%s (%s)", r.ProviderWithoutVersions, strings.Join(versions, ", ")) } rows = append(rows, []string{r.Vuln, prov, r.PublishedDate, r.Severity, r.Reference}) } // sort rows by each column sort.Slice(rows, func(i, j int) bool { for k := range rows[i] { if rows[i][k] != rows[j][k] { return rows[i][k] < rows[j][k] } } return false }) return rows } func getOSVersions(oss []dbsearch.OperatingSystem) []string { var versions []string for _, os := range oss { versions = append(versions, os.Version) } return versions } func getPrimaryReference(refs []v6.Reference) string { if len(refs) > 0 { return refs[0].URL } return "" } func getDate(t *time.Time) string { if t != nil && !t.IsZero() { return t.Format("2006-01-02") } return "" } ================================================ FILE: cmd/grype/cli/commands/db_search_vuln_test.go ================================================ package commands import ( "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" v6 "github.com/anchore/grype/grype/db/v6" ) func TestGetOSVersions(t *testing.T) { tests := []struct { name string input []dbsearch.OperatingSystem expected []string }{ { name: "empty list", input: []dbsearch.OperatingSystem{}, expected: nil, }, { name: "single os", input: []dbsearch.OperatingSystem{ { Name: "debian", Version: "11", }, }, expected: []string{"11"}, }, { name: "multiple os", input: []dbsearch.OperatingSystem{ { Name: "ubuntu", Version: "16.04", }, { Name: "ubuntu", Version: "22.04", }, { Name: "ubuntu", Version: "24.04", }, }, expected: []string{"16.04", "22.04", "24.04"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := getOSVersions(tt.input) require.Equal(t, tt.expected, actual) }) } } func TestGetPrimaryReference(t *testing.T) { tests := []struct { name string input []v6.Reference expected string }{ { name: "empty list", input: []v6.Reference{}, expected: "", }, { name: "single reference", input: []v6.Reference{ { URL: "https://example.com/vuln/123", Tags: []string{"primary"}, }, }, expected: "https://example.com/vuln/123", }, { name: "multiple references", input: []v6.Reference{ { URL: "https://example.com/vuln/123", Tags: []string{"primary"}, }, { URL: "https://example.com/advisory/123", Tags: []string{"secondary"}, }, }, expected: "https://example.com/vuln/123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := getPrimaryReference(tt.input) require.Equal(t, tt.expected, actual) }) } } func TestGetDate(t *testing.T) { tests := []struct { name string input *time.Time expected string }{ { name: "nil time", input: nil, expected: "", }, { name: "zero time", input: &time.Time{}, expected: "", }, { name: "valid time", input: timePtr(time.Date(2023, 5, 15, 0, 0, 0, 0, time.UTC)), expected: "2023-05-15", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := getDate(tt.input) require.Equal(t, tt.expected, actual) }) } } func timePtr(t time.Time) *time.Time { return &t } ================================================ FILE: cmd/grype/cli/commands/db_status.go ================================================ package commands import ( "encoding/json" "fmt" "io" "os" "time" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/grype/vulnerability" ) type dbStatusOptions struct { Output string `yaml:"output" json:"output" mapstructure:"output"` options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } var _ clio.FlagAdder = (*dbStatusOptions)(nil) func (d *dbStatusOptions) AddFlags(flags clio.FlagSet) { flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, json])") } func DBStatus(app clio.Application) *cobra.Command { opts := &dbStatusOptions{ Output: textOutputFormat, DatabaseCommand: *options.DefaultDatabaseCommand(app.ID()), } cmd := &cobra.Command{ Use: "status", Short: "Display database status and metadata", Args: cobra.ExactArgs(0), PreRunE: disableUI(app), RunE: func(_ *cobra.Command, _ []string) error { return runDBStatus(*opts) }, } // prevent from being shown in the grype config type configWrapper struct { Hidden *dbStatusOptions `json:"-" yaml:"-" mapstructure:"-"` *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand}) } func runDBStatus(opts dbStatusOptions) error { client, err := distribution.NewClient(opts.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } c, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } status := c.Status() if err := presentDBStatus(opts.Output, os.Stdout, status); err != nil { return fmt.Errorf("failed to present db status information: %+v", err) } return status.Error } func presentDBStatus(format string, writer io.Writer, status vulnerability.ProviderStatus) error { switch format { case textOutputFormat: fmt.Fprintln(writer, "Path: ", status.Path) fmt.Fprintln(writer, "Schema: ", status.SchemaVersion) fmt.Fprintln(writer, "Built: ", status.Built.Format(time.RFC3339)) if status.From != "" { fmt.Fprintln(writer, "From: ", status.From) } fmt.Fprintln(writer, "Status: ", renderStoreValidation(status)) case jsonOutputFormat: enc := json.NewEncoder(writer) enc.SetEscapeHTML(false) enc.SetIndent("", " ") if err := enc.Encode(&status); err != nil { return fmt.Errorf("failed to db status information: %+v", err) } default: return fmt.Errorf("unsupported output format: %s", format) } return nil } func renderStoreValidation(status vulnerability.ProviderStatus) string { if status.Error != nil { return "invalid" } return "valid" } ================================================ FILE: cmd/grype/cli/commands/db_status_test.go ================================================ package commands import ( "bytes" "errors" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func TestPresentDBStatus(t *testing.T) { validStatus := vulnerability.ProviderStatus{ Path: "/Users/test/Library/Caches/grype/db/6/vulnerability.db", From: "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", SchemaVersion: "6.0.0", Built: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC), Error: nil, } invalidStatus := vulnerability.ProviderStatus{ Path: "/Users/test/Library/Caches/grype/db/6/vulnerability.db", From: "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", SchemaVersion: "6.0.0", Built: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC), Error: errors.New("checksum mismatch"), } tests := []struct { name string format string status vulnerability.ProviderStatus expectedText string expectedErr require.ErrorAssertionFunc }{ { name: "valid status, text format", format: textOutputFormat, status: validStatus, expectedText: `Path: /Users/test/Library/Caches/grype/db/6/vulnerability.db Schema: 6.0.0 Built: 2024-11-27T14:43:17Z From: https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8 Status: valid `, expectedErr: require.NoError, }, { name: "invalid status, text format", format: textOutputFormat, status: invalidStatus, expectedText: `Path: /Users/test/Library/Caches/grype/db/6/vulnerability.db Schema: 6.0.0 Built: 2024-11-27T14:43:17Z From: https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8 Status: invalid `, expectedErr: require.NoError, }, { name: "valid status, JSON format", format: jsonOutputFormat, status: validStatus, expectedText: `{ "schemaVersion": "6.0.0", "from": "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", "built": "2024-11-27T14:43:17Z", "path": "/Users/test/Library/Caches/grype/db/6/vulnerability.db", "valid": true } `, expectedErr: require.NoError, }, { name: "invalid status, JSON format", format: jsonOutputFormat, status: invalidStatus, expectedText: `{ "schemaVersion": "6.0.0", "from": "https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8", "built": "2024-11-27T14:43:17Z", "path": "/Users/test/Library/Caches/grype/db/6/vulnerability.db", "valid": false, "error": "checksum mismatch" } `, expectedErr: require.NoError, }, { name: "unsupported format", format: "unsupported", status: validStatus, expectedErr: requireErrorContains("unsupported output format"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectedErr == nil { tt.expectedErr = require.NoError } writer := &bytes.Buffer{} err := presentDBStatus(tt.format, writer, tt.status) tt.expectedErr(t, err) if err != nil { return } assert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String())) }) } } ================================================ FILE: cmd/grype/cli/commands/db_update.go ================================================ package commands import ( "fmt" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" ) func DBUpdate(app clio.Application) *cobra.Command { opts := options.DefaultDatabaseCommand(app.ID()) cmd := &cobra.Command{ Use: "update", Short: "Download and install the latest vulnerability database", Args: cobra.ExactArgs(0), PreRunE: func(_ *cobra.Command, _ []string) error { // DB commands should not opt into the low-pass check filter opts.DB.MaxUpdateCheckFrequency = 0 return nil }, RunE: func(_ *cobra.Command, _ []string) error { return runDBUpdate(*opts) }, } // prevent from being shown in the grype config type configWrapper struct { *options.DatabaseCommand `yaml:",inline" mapstructure:",squash"` } return app.SetupCommand(cmd, &configWrapper{opts}) } func runDBUpdate(opts options.DatabaseCommand) error { cfg := opts.ToClientConfig() // we need to have this set to true to force the update call to try to update // regardless of what the user provided in order for update checks to fail if !cfg.RequireUpdateCheck { log.Warn("overriding db update check") cfg.RequireUpdateCheck = true } client, err := distribution.NewClient(cfg) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) } c, err := installation.NewCurator(opts.ToCuratorConfig(), client) if err != nil { return fmt.Errorf("unable to create curator: %w", err) } updated, err := c.Update() if err != nil { return fmt.Errorf("unable to update vulnerability database: %w", err) } result := "No vulnerability database update available\n" if updated { result = "Vulnerability database updated to latest version!\n" } log.Debugf("completed db update check with result: %s", result) bus.Report(result) return nil } ================================================ FILE: cmd/grype/cli/commands/explain.go ================================================ package commands import ( "encoding/json" "fmt" "os" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/grype/presenter/explain" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" ) type explainOptions struct { CVEIDs []string `yaml:"cve-ids" json:"cve-ids" mapstructure:"cve-ids"` } var _ clio.FlagAdder = (*explainOptions)(nil) func (d *explainOptions) AddFlags(flags clio.FlagSet) { flags.StringArrayVarP(&d.CVEIDs, "id", "", "CVE IDs to explain") } func Explain(app clio.Application) *cobra.Command { opts := &explainOptions{} cmd := &cobra.Command{ Use: "explain --id [VULNERABILITY ID]", Short: "Ask grype to explain a set of findings", PreRunE: disableUI(app), RunE: func(_ *cobra.Command, _ []string) error { log.Warn("grype explain is a prototype feature and is subject to change") isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() if err != nil { log.Warnf("unable to determine if there is piped input: %+v", err) isStdinPipeOrRedirect = false } if isStdinPipeOrRedirect { // TODO: eventually detect different types of input; for now assume grype json var parseResult models.Document decoder := json.NewDecoder(os.Stdin) err := decoder.Decode(&parseResult) if err != nil { return fmt.Errorf("unable to parse piped input: %+v", err) } explainer := explain.NewVulnerabilityExplainer(os.Stdout, &parseResult) return explainer.ExplainByID(opts.CVEIDs) } // perform a scan, then explain requested CVEs // TODO: implement return fmt.Errorf("requires grype json on stdin, please run 'grype -o json ... | grype explain ...'") }, } // prevent from being shown in the grype config type configWrapper struct { Opts *explainOptions `json:"-" yaml:"-" mapstructure:"-"` } return app.SetupCommand(cmd, &configWrapper{opts}) } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/affected_packages.go ================================================ package dbsearch import ( "errors" "fmt" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) var ErrNoSearchCriteria = errors.New("must provide at least one of vulnerability or package to search for") // AffectedPackage represents a package affected by a vulnerability type AffectedPackage struct { // Vulnerability is the core advisory record for a single known vulnerability from a specific provider. Vulnerability VulnerabilityInfo `json:"vulnerability"` // AffectedPackageInfo is the detailed information about the affected package AffectedPackageInfo `json:",inline"` } type AffectedPackageInfo struct { // TODO: remove this when namespace is no longer used Model *v6.AffectedPackageHandle `json:"-"` // tracking package handle info is necessary for namespace lookup (note CPE handles are not tracked) // OS identifies the operating system release that the affected package is released for OS *OperatingSystem `json:"os,omitempty"` // Package identifies the name of the package in a specific ecosystem affected by the vulnerability Package *Package `json:"package,omitempty"` // CPE is a Common Platform Enumeration that is affected by the vulnerability CPE *CPE `json:"cpe,omitempty"` // Namespace is a holdover value from the v5 DB schema that combines provider and search methods into a single value // // Deprecated: this field will be removed in a later version of the search schema Namespace string `json:"namespace"` // Detail is the detailed information about the affected package Detail v6.PackageBlob `json:"detail"` } // Package represents a package name within a known ecosystem, such as "python" or "golang". type Package struct { // Name is the name of the package within the ecosystem Name string `json:"name"` // Ecosystem is the tooling and language ecosystem that the package is released within Ecosystem string `json:"ecosystem"` } // CPE is a Common Platform Enumeration that identifies a package type CPE v6.Cpe func (c *CPE) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("%q", c.String())), nil } func (c *CPE) String() string { if c == nil { return "" } return v6.Cpe(*c).String() } type AffectedPackagesOptions struct { Vulnerability v6.VulnerabilitySpecifiers Package v6.PackageSpecifiers CPE v6.PackageSpecifiers OS v6.OSSpecifiers AllowBroadCPEMatching bool RecordLimit int FixedStates []string } type affectedPackageWithDecorations struct { v6.AffectedPackageHandle vulnerabilityDecorations } func (a *affectedPackageWithDecorations) getCVEs() []string { if a == nil { return nil } return getCVEs(a.Vulnerability) } type affectedCPEWithDecorations struct { v6.AffectedCPEHandle vulnerabilityDecorations } func (a *affectedCPEWithDecorations) getCVEs() []string { if a == nil { return nil } return getCVEs(a.Vulnerability) } func newAffectedPackageRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []AffectedPackage) { for i := range affectedPkgs { pkg := affectedPkgs[i] var detail v6.PackageBlob if pkg.BlobValue != nil { detail = *pkg.BlobValue } if pkg.Vulnerability == nil { log.Errorf("affected package record missing vulnerability: %+v", pkg) continue } rows = append(rows, AffectedPackage{ Vulnerability: newVulnerabilityInfo(*pkg.Vulnerability, pkg.vulnerabilityDecorations), AffectedPackageInfo: AffectedPackageInfo{ Model: &pkg.AffectedPackageHandle, OS: toOS(pkg.OperatingSystem), Package: toPackage(pkg.Package), Namespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle), Detail: detail, }, }) } for _, ac := range affectedCPEs { var detail v6.PackageBlob if ac.BlobValue != nil { detail = *ac.BlobValue } if ac.Vulnerability == nil { log.Errorf("affected CPE record missing vulnerability: %+v", ac) continue } var c *CPE if ac.CPE != nil { cv := CPE(*ac.CPE) c = &cv } rows = append(rows, AffectedPackage{ // tracking model information is not possible with CPE handles Vulnerability: newVulnerabilityInfo(*ac.Vulnerability, ac.vulnerabilityDecorations), AffectedPackageInfo: AffectedPackageInfo{ CPE: c, Namespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD Detail: detail, }, }) } return rows } func toPackage(pkg *v6.Package) *Package { if pkg == nil { return nil } return &Package{ Name: pkg.Name, Ecosystem: pkg.Ecosystem, } } func toOS(os *v6.OperatingSystem) *OperatingSystem { if os == nil { return nil } return &OperatingSystem{ Name: os.Name, Version: os.Version(), } } func FindAffectedPackages(reader interface { v6.AffectedPackageStoreReader v6.AffectedCPEStoreReader v6.VulnerabilityDecoratorStoreReader }, criteria AffectedPackagesOptions, ) ([]AffectedPackage, error) { allAffectedPkgs, allAffectedCPEs, err := findAffectedPackages(reader, criteria) if err != nil { return nil, err } if len(criteria.FixedStates) > 0 { allAffectedPkgs = filterByFixedStateForPackages(allAffectedPkgs, criteria.FixedStates) allAffectedCPEs = filterByFixedStateForCPEs(allAffectedCPEs, criteria.FixedStates) } return newAffectedPackageRows(allAffectedPkgs, allAffectedCPEs), nil } func findAffectedPackages(reader interface { //nolint:funlen,gocognit v6.AffectedPackageStoreReader v6.AffectedCPEStoreReader v6.VulnerabilityDecoratorStoreReader }, config AffectedPackagesOptions, ) ([]affectedPackageWithDecorations, []affectedCPEWithDecorations, error) { var allAffectedPkgs []affectedPackageWithDecorations var allAffectedCPEs []affectedCPEWithDecorations pkgSpecs := config.Package cpeSpecs := config.CPE osSpecs := config.OS vulnSpecs := config.Vulnerability if config.RecordLimit == 0 { log.Warn("no record limit set! For queries with large result sets this may result in performance issues") } if len(vulnSpecs) == 0 && len(pkgSpecs) == 0 && len(cpeSpecs) == 0 { return nil, nil, ErrNoSearchCriteria } // don't allow for searching by any package AND any CPE AND any vulnerability AND any OS. Since these searches // are oriented by primarily package, we only want to have ANY package/CPE when there is a vulnerability or OS specified. if len(vulnSpecs) > 0 || !osSpecs.IsAny() { if len(pkgSpecs) == 0 { pkgSpecs = []*v6.PackageSpecifier{v6.AnyPackageSpecified} } if len(cpeSpecs) == 0 { cpeSpecs = []*v6.PackageSpecifier{v6.AnyPackageSpecified} } } // we have multiple return points that return actual values, using a defer to decorate any given results // ensures that all paths are handled the same way. defer func() { for i := range allAffectedPkgs { decorateVulnerabilities(reader, &allAffectedPkgs[i]) } for i := range allAffectedCPEs { decorateVulnerabilities(reader, &allAffectedCPEs[i]) } }() for i := range pkgSpecs { pkgSpec := pkgSpecs[i] log.WithFields("vuln", vulnSpecs, "pkg", pkgSpec, "os", osSpecs).Debug("searching for affected packages") affectedPkgs, err := reader.GetAffectedPackages(pkgSpec, &v6.GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadPackageCPEs: false, PreloadVulnerability: true, PreloadBlob: true, OSs: osSpecs, Vulnerabilities: vulnSpecs, AllowBroadCPEMatching: config.AllowBroadCPEMatching, Limit: config.RecordLimit, }) for i := range affectedPkgs { allAffectedPkgs = append(allAffectedPkgs, affectedPackageWithDecorations{ AffectedPackageHandle: affectedPkgs[i], }) } if err != nil { if errors.Is(err, v6.ErrLimitReached) { return allAffectedPkgs, allAffectedCPEs, err } return nil, nil, fmt.Errorf("unable to get affected packages for %s: %w", vulnSpecs, err) } } if osSpecs.IsAny() { for i := range cpeSpecs { cpeSpec := cpeSpecs[i] var searchCPE *cpe.Attributes if cpeSpec != nil { searchCPE = cpeSpec.CPE } log.WithFields("vuln", vulnSpecs, "cpe", cpeSpec).Debug("searching for affected packages") affectedCPEs, err := reader.GetAffectedCPEs(searchCPE, &v6.GetCPEOptions{ PreloadCPE: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: vulnSpecs, AllowBroadCPEMatching: config.AllowBroadCPEMatching, Limit: config.RecordLimit, }) for i := range affectedCPEs { allAffectedCPEs = append(allAffectedCPEs, affectedCPEWithDecorations{ AffectedCPEHandle: affectedCPEs[i], }) } if err != nil { if errors.Is(err, v6.ErrLimitReached) { return allAffectedPkgs, allAffectedCPEs, err } return nil, nil, fmt.Errorf("unable to get affected cpes for %s: %w", vulnSpecs, err) } } } return allAffectedPkgs, allAffectedCPEs, nil } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go ================================================ package dbsearch import ( "bytes" "encoding/json" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/syft/syft/cpe" ) func TestAffectedPackageTableRowMarshalJSON(t *testing.T) { row := AffectedPackage{ Vulnerability: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{ ID: "CVE-1234-5678", Description: "Test vulnerability", }, Provider: "provider1", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, AffectedPackageInfo: AffectedPackageInfo{ Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, Namespace: "namespace1", Detail: v6.PackageBlob{ CVEs: []string{"CVE-1234-5678"}, Qualifiers: &v6.PackageQualifiers{ RpmModularity: ptr("modularity"), PlatformCPEs: []string{"platform-cpe-1"}, }, Ranges: []v6.Range{ { Version: v6.Version{ Type: "semver", Constraint: ">=1.0.0, <2.0.0", }, Fix: &v6.Fix{ Version: "1.2.0", State: "fixed", }, }, }, }, }, } buf := bytes.Buffer{} enc := json.NewEncoder(&buf) enc.SetIndent("", " ") enc.SetEscapeHTML(false) err := enc.Encode(row) require.NoError(t, err) expectedJSON := `{ "vulnerability": { "id": "CVE-1234-5678", "description": "Test vulnerability", "provider": "provider1", "status": "active", "published_date": "2023-01-01T00:00:00Z", "modified_date": "2023-02-01T00:00:00Z", "known_exploited": [ { "cve": "CVE-1234-5678", "vendor_project": "LinuxFoundation", "product": "Linux", "date_added": "2025-02-02", "required_action": "Yes", "due_date": "2025-02-02", "known_ransomware_campaign_use": "Known", "notes": "note!", "urls": [ "https://example.com" ], "cwes": [ "CWE-1234" ] } ], "epss": [ { "cve": "CVE-1234-5678", "epss": 0.893, "percentile": 0.99, "date": "2025-02-24" } ] }, "package": { "name": "pkg1", "ecosystem": "ecosystem1" }, "cpe": "cpe:2.3:a:vendor1:product1:*:*:*:*:*:*:*:*", "namespace": "namespace1", "detail": { "cves": [ "CVE-1234-5678" ], "qualifiers": { "rpm_modularity": "modularity", "platform_cpes": [ "platform-cpe-1" ] }, "ranges": [ { "version": { "type": "semver", "constraint": ">=1.0.0, <2.0.0" }, "fix": { "version": "1.2.0", "state": "fixed" } } ] } } ` if diff := cmp.Diff(expectedJSON, buf.String()); diff != "" { t.Errorf("unexpected JSON (-want +got):\n%s", diff) } } func TestNewAffectedPackageRows(t *testing.T) { affectedPkgs := []affectedPackageWithDecorations{ { AffectedPackageHandle: v6.AffectedPackageHandle{ Package: &v6.Package{Name: "pkg1", Ecosystem: "ecosystem1"}, OperatingSystem: &v6.OperatingSystem{ Name: "Linux", MajorVersion: "5", MinorVersion: "10", }, Vulnerability: &v6.VulnerabilityHandle{ Name: "CVE-1234-5678", Provider: &v6.Provider{ID: "provider1"}, Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), BlobValue: &v6.VulnerabilityBlob{ Description: "Test vulnerability", Severities: []v6.Severity{ { Scheme: "CVSS_V3", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 9.8, }, }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, BlobValue: &v6.PackageBlob{ CVEs: []string{"CVE-1234-5678"}, Qualifiers: &v6.PackageQualifiers{ RpmModularity: ptr("modularity"), PlatformCPEs: []string{"platform-cpe-1"}, }, Ranges: []v6.Range{ { Version: v6.Version{ Type: "semver", Constraint: ">=1.0.0, <2.0.0", }, Fix: &v6.Fix{ Version: "1.2.0", State: "fixed", }, }, }, }, }, vulnerabilityDecorations: vulnerabilityDecorations{ KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, }, } affectedCPEs := []affectedCPEWithDecorations{ { AffectedCPEHandle: v6.AffectedCPEHandle{ CPE: &v6.Cpe{Part: "a", Vendor: "vendor1", Product: "product1"}, Vulnerability: &v6.VulnerabilityHandle{ Name: "CVE-9876-5432", Provider: &v6.Provider{ID: "provider2"}, BlobValue: &v6.VulnerabilityBlob{Description: "CPE vulnerability description"}, }, BlobValue: &v6.PackageBlob{ CVEs: []string{"CVE-9876-5432"}, Ranges: []v6.Range{ { Version: v6.Version{ Type: "rpm", Constraint: ">=2.0.0, <3.0.0", }, Fix: &v6.Fix{ Version: "2.5.0", State: "fixed", }, }, }, }, }, vulnerabilityDecorations: vulnerabilityDecorations{ KnownExploited: []KnownExploited{ { CVE: "CVE-9876-5432", VendorProject: "vendor1", Product: "product1", DateAdded: "2025-02-03", RequiredAction: "Yes", DueDate: "2025-02-03", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-5678"}, }, }, EPSS: []EPSS{ { CVE: "CVE-9876-5432", EPSS: 0.938, Percentile: 0.9222, Date: "2025-02-25", }, }, }, }, } rows := newAffectedPackageRows(affectedPkgs, affectedCPEs) expected := []AffectedPackage{ { Vulnerability: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{ Description: "Test vulnerability", Severities: []v6.Severity{ { Scheme: "CVSS_V3", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 9.8, }, }, Source: "nvd@nist.gov", Rank: 1, }, }, }, Severity: "critical", Provider: "provider1", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, AffectedPackageInfo: AffectedPackageInfo{ OS: &OperatingSystem{Name: "Linux", Version: "5.10"}, Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, Namespace: "provider1:distro:Linux:5.10", Detail: v6.PackageBlob{ CVEs: []string{"CVE-1234-5678"}, Qualifiers: &v6.PackageQualifiers{ RpmModularity: ptr("modularity"), PlatformCPEs: []string{"platform-cpe-1"}, }, Ranges: []v6.Range{ { Version: v6.Version{ Type: "semver", Constraint: ">=1.0.0, <2.0.0", }, Fix: &v6.Fix{ Version: "1.2.0", State: "fixed", }, }, }, }, }, }, { Vulnerability: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "CPE vulnerability description"}, Severity: "unknown", Provider: "provider2", KnownExploited: []KnownExploited{ { CVE: "CVE-9876-5432", VendorProject: "vendor1", Product: "product1", DateAdded: "2025-02-03", RequiredAction: "Yes", DueDate: "2025-02-03", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-5678"}, }, }, EPSS: []EPSS{ { CVE: "CVE-9876-5432", EPSS: 0.938, Percentile: 0.9222, Date: "2025-02-25", }, }, }, AffectedPackageInfo: AffectedPackageInfo{ CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, Namespace: "provider2:cpe", Detail: v6.PackageBlob{ CVEs: []string{"CVE-9876-5432"}, Ranges: []v6.Range{ { Version: v6.Version{ Type: "rpm", Constraint: ">=2.0.0, <3.0.0", }, Fix: &v6.Fix{ Version: "2.5.0", State: "fixed", }, }, }, }, }, }, } if diff := cmp.Diff(expected, rows, cmpOpts()...); diff != "" { t.Errorf("unexpected rows (-want +got):\n%s", diff) } } func TestAffectedPackages(t *testing.T) { mockReader := new(affectedMockReader) mockReader.On("GetAffectedPackages", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{ { Package: &v6.Package{Name: "pkg1", Ecosystem: "ecosystem1"}, OperatingSystem: &v6.OperatingSystem{ Name: "Linux", MajorVersion: "5", MinorVersion: "10", }, Vulnerability: &v6.VulnerabilityHandle{ Name: "CVE-1234-5678", Provider: &v6.Provider{ID: "provider1"}, Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), BlobValue: &v6.VulnerabilityBlob{Description: "Test vulnerability"}, }, BlobValue: &v6.PackageBlob{ CVEs: []string{"CVE-1234-5678"}, Ranges: []v6.Range{ { Version: v6.Version{ Type: "semver", Constraint: ">=1.0.0, <2.0.0", }, Fix: &v6.Fix{ Version: "1.2.0", State: "fixed", }, }, }, }, }, }, nil) mockReader.On("GetAffectedCPEs", mock.Anything, mock.Anything).Return([]v6.AffectedCPEHandle{ { CPE: &v6.Cpe{Part: "a", Vendor: "vendor1", Product: "product1"}, Vulnerability: &v6.VulnerabilityHandle{ Name: "CVE-9876-5432", Provider: &v6.Provider{ID: "provider2"}, BlobValue: &v6.VulnerabilityBlob{Description: "CPE vulnerability description"}, }, BlobValue: &v6.PackageBlob{ CVEs: []string{"CVE-9876-5432"}, Ranges: []v6.Range{ { Version: v6.Version{ Type: "rpm", Constraint: ">=2.0.0, <3.0.0", }, Fix: &v6.Fix{ Version: "2.5.0", State: "fixed", }, }, }, }, }, }, nil) mockReader.On("GetKnownExploitedVulnerabilities", "CVE-1234-5678").Return([]v6.KnownExploitedVulnerabilityHandle{ { Cve: "CVE-1234-5678", BlobValue: &v6.KnownExploitedVulnerabilityBlob{ Cve: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)), RequiredAction: "Yes", DueDate: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)), KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, }, nil) mockReader.On("GetKnownExploitedVulnerabilities", "CVE-9876-5432").Return([]v6.KnownExploitedVulnerabilityHandle{ { Cve: "CVE-9876-5432", BlobValue: &v6.KnownExploitedVulnerabilityBlob{ Cve: "CVE-9876-5432", VendorProject: "vendor1", Product: "product1", DateAdded: ptr(time.Date(2025, 2, 3, 0, 0, 0, 0, time.UTC)), RequiredAction: "Yes", DueDate: ptr(time.Date(2025, 2, 3, 0, 0, 0, 0, time.UTC)), KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-5678"}, }, }, }, nil) mockReader.On("GetEpss", "CVE-1234-5678").Return([]v6.EpssHandle{ { Cve: "CVE-1234-5678", Epss: 0.893, Percentile: 0.99, Date: time.Date(2025, 2, 24, 0, 0, 0, 0, time.UTC), }, }, nil) mockReader.On("GetEpss", "CVE-9876-5432").Return([]v6.EpssHandle{ { Cve: "CVE-9876-5432", Epss: 0.938, Percentile: 0.9222, Date: time.Date(2025, 2, 25, 0, 0, 0, 0, time.UTC), }, }, nil) criteria := AffectedPackagesOptions{ Vulnerability: v6.VulnerabilitySpecifiers{ {Name: "CVE-1234-5678"}, }, } results, err := FindAffectedPackages(mockReader, criteria) require.NoError(t, err) expected := []AffectedPackage{ { Vulnerability: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "Test vulnerability"}, Severity: "unknown", Provider: "provider1", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, AffectedPackageInfo: AffectedPackageInfo{ OS: &OperatingSystem{Name: "Linux", Version: "5.10"}, Package: &Package{Name: "pkg1", Ecosystem: "ecosystem1"}, Namespace: "provider1:distro:Linux:5.10", Detail: v6.PackageBlob{ CVEs: []string{"CVE-1234-5678"}, Ranges: []v6.Range{ { Version: v6.Version{ Type: "semver", Constraint: ">=1.0.0, <2.0.0", }, Fix: &v6.Fix{ Version: "1.2.0", State: "fixed", }, }, }, }, }, }, { Vulnerability: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{Description: "CPE vulnerability description"}, Severity: "unknown", Provider: "provider2", KnownExploited: []KnownExploited{ { CVE: "CVE-9876-5432", VendorProject: "vendor1", Product: "product1", DateAdded: "2025-02-03", RequiredAction: "Yes", DueDate: "2025-02-03", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-5678"}, }, }, EPSS: []EPSS{ { CVE: "CVE-9876-5432", EPSS: 0.938, Percentile: 0.9222, Date: "2025-02-25", }, }, }, AffectedPackageInfo: AffectedPackageInfo{ CPE: &CPE{Part: "a", Vendor: "vendor1", Product: "product1"}, Namespace: "provider2:cpe", Detail: v6.PackageBlob{ CVEs: []string{"CVE-9876-5432"}, Ranges: []v6.Range{ { Version: v6.Version{ Type: "rpm", Constraint: ">=2.0.0, <3.0.0", }, Fix: &v6.Fix{ Version: "2.5.0", State: "fixed", }, }, }, }, }, }, } if diff := cmp.Diff(expected, results, cmpOpts()...); diff != "" { t.Errorf("unexpected results (-want +got):\n%s", diff) } } func TestFindAffectedPackages(t *testing.T) { // this test is not meant to check the correctness of the results relative to the reader but instead make certain // that the correct calls are made to the reader based on the search criteria (we're wired up correctly). // Additional verifications are made to check that the combinations of different specs are handled correctly. type pkgCall struct { pkg *v6.PackageSpecifier options *v6.GetPackageOptions } type cpeCall struct { cpe *cpe.Attributes options *v6.GetCPEOptions } testCases := []struct { name string config AffectedPackagesOptions expectedPkgCalls []pkgCall expectedCPECalls []cpeCall expectedErr error }{ { name: "no search criteria", config: AffectedPackagesOptions{}, expectedErr: ErrNoSearchCriteria, }, { name: "os spec alone is not enough", config: AffectedPackagesOptions{ OS: v6.OSSpecifiers{ {Name: "ubuntu", MajorVersion: "20", MinorVersion: "04"}, }, }, expectedErr: ErrNoSearchCriteria, }, { name: "vuln spec provided", config: AffectedPackagesOptions{ Vulnerability: v6.VulnerabilitySpecifiers{ {Name: "CVE-2023-0001"}, }, }, expectedPkgCalls: []pkgCall{ { pkg: nil, options: &v6.GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: v6.VulnerabilitySpecifiers{ {Name: "CVE-2023-0001"}, }, Limit: 0, }, }, }, expectedCPECalls: []cpeCall{ { cpe: nil, options: &v6.GetCPEOptions{ PreloadCPE: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: v6.VulnerabilitySpecifiers{ {Name: "CVE-2023-0001"}, }, Limit: 0, }, }, }, }, { name: "only cpe spec provided", config: AffectedPackagesOptions{ Package: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}}, }, CPE: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor2", Product: "product2"}}, }, }, expectedPkgCalls: []pkgCall{ { pkg: &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}}, options: &v6.GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: nil, Limit: 0, }, }, }, expectedCPECalls: []cpeCall{ { cpe: &cpe.Attributes{Part: "a", Vendor: "vendor2", Product: "product2"}, options: &v6.GetCPEOptions{ PreloadCPE: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: nil, Limit: 0, }, }, }, expectedErr: nil, }, { name: "cpe + os spec provided", config: AffectedPackagesOptions{ Package: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}}, }, CPE: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor2", Product: "product2"}}, }, OS: v6.OSSpecifiers{ {Name: "debian", MajorVersion: "10"}, // this prevents an agnostic CPE search }, }, expectedPkgCalls: []pkgCall{ { pkg: &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Vendor: "vendor1", Product: "product1"}}, options: &v6.GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: nil, OSs: v6.OSSpecifiers{ {Name: "debian", MajorVersion: "10"}, }, Limit: 0, }, }, }, expectedCPECalls: nil, expectedErr: nil, }, { name: "pkg spec provided", config: AffectedPackagesOptions{ Package: v6.PackageSpecifiers{ {Name: "test-package", Ecosystem: "npm"}, }, }, expectedPkgCalls: []pkgCall{ { pkg: &v6.PackageSpecifier{Name: "test-package", Ecosystem: "npm"}, options: &v6.GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadVulnerability: true, PreloadBlob: true, Vulnerabilities: nil, Limit: 0, }, }, }, expectedCPECalls: nil, }, { name: "pkg and os specs provided", config: AffectedPackagesOptions{ Package: v6.PackageSpecifiers{ {Name: "test-package", Ecosystem: "npm"}, }, OS: v6.OSSpecifiers{ {Name: "debian", MajorVersion: "10"}, }, }, expectedPkgCalls: []pkgCall{ { pkg: &v6.PackageSpecifier{Name: "test-package", Ecosystem: "npm"}, options: &v6.GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadVulnerability: true, PreloadBlob: true, OSs: v6.OSSpecifiers{ {Name: "debian", MajorVersion: "10"}, }, Limit: 0, }, }, }, expectedCPECalls: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { m := new(affectedMockReader) defer m.AssertExpectations(t) for _, expected := range tc.expectedPkgCalls { m.On("GetAffectedPackages", expected.pkg, mock.MatchedBy(func(actual *v6.GetPackageOptions) bool { return cmp.Equal(actual, expected.options) })).Return([]v6.AffectedPackageHandle{}, nil).Once() } for _, expected := range tc.expectedCPECalls { m.On("GetAffectedCPEs", expected.cpe, mock.MatchedBy(func(actual *v6.GetCPEOptions) bool { return cmp.Equal(actual, expected.options) })).Return([]v6.AffectedCPEHandle{}, nil).Once() } _, _, err := findAffectedPackages(m, tc.config) if tc.expectedErr != nil { require.ErrorIs(t, err, tc.expectedErr) } else { require.NoError(t, err) } }) } } type affectedMockReader struct { mock.Mock } func (m *affectedMockReader) GetAffectedPackages(pkgSpec *v6.PackageSpecifier, options *v6.GetPackageOptions) ([]v6.AffectedPackageHandle, error) { args := m.Called(pkgSpec, options) return args.Get(0).([]v6.AffectedPackageHandle), args.Error(1) } func (m *affectedMockReader) GetAffectedCPEs(cpeSpec *cpe.Attributes, options *v6.GetCPEOptions) ([]v6.AffectedCPEHandle, error) { args := m.Called(cpeSpec, options) return args.Get(0).([]v6.AffectedCPEHandle), args.Error(1) } func (m *affectedMockReader) GetKnownExploitedVulnerabilities(cve string) ([]v6.KnownExploitedVulnerabilityHandle, error) { args := m.Called(cve) return args.Get(0).([]v6.KnownExploitedVulnerabilityHandle), args.Error(1) } func (m *affectedMockReader) GetEpss(cve string) ([]v6.EpssHandle, error) { args := m.Called(cve) return args.Get(0).([]v6.EpssHandle), args.Error(1) } func (m *affectedMockReader) GetCWEs(cve string) ([]v6.CWEHandle, error) { args := m.Called(cve) return args.Get(0).([]v6.CWEHandle), args.Error(1) } func ptr[T any](t T) *T { return &t } func TestGetFixStateFromPackageBlob(t *testing.T) { tests := []struct { name string blob *v6.PackageBlob expected string }{ { name: "nil blob returns unknown", blob: nil, expected: "unknown", }, { name: "empty blob returns unknown", blob: &v6.PackageBlob{}, expected: "unknown", }, { name: "blob with fixed status", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.FixedStatus, Version: "1.2.3", }, }, }, }, expected: "fixed", }, { name: "blob with not-fixed status", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.NotFixedStatus, }, }, }, }, expected: "not-fixed", }, { name: "blob with wont-fix status", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.WontFixStatus, }, }, }, }, expected: "wont-fix", }, { name: "blob with no fix returns unknown", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: nil, }, }, }, expected: "unknown", }, { name: "blob with mixed statuses prefers fixed", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.NotFixedStatus, }, }, { Fix: &v6.Fix{ State: v6.FixedStatus, Version: "2.0.0", }, }, }, }, expected: "fixed", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getFixStateFromPackageBlob(tt.blob) assert.Equal(t, tt.expected, result) }) } } func TestFilterByFixedStateForPackages(t *testing.T) { tests := []struct { name string packages []affectedPackageWithDecorations fixedStates []string expectedLen int }{ { name: "empty fixed states returns all packages", packages: []affectedPackageWithDecorations{ {AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: &v6.PackageBlob{}}}, {AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: &v6.PackageBlob{}}}, }, fixedStates: []string{}, expectedLen: 2, }, { name: "filter by fixed state", packages: []affectedPackageWithDecorations{ makeAffectedPackageWithFixState(v6.FixedStatus), makeAffectedPackageWithFixState(v6.NotFixedStatus), }, fixedStates: []string{"fixed"}, expectedLen: 1, }, { name: "filter by multiple states", packages: []affectedPackageWithDecorations{ makeAffectedPackageWithFixState(v6.FixedStatus), makeAffectedPackageWithFixState(v6.NotFixedStatus), makeAffectedPackageWithFixState(v6.WontFixStatus), }, fixedStates: []string{"fixed", "wont-fix"}, expectedLen: 2, }, { name: "filter with no matches", packages: []affectedPackageWithDecorations{ makeAffectedPackageWithFixState(v6.NotFixedStatus), }, fixedStates: []string{"fixed"}, expectedLen: 0, }, { name: "packages with nil blob are filtered out", packages: []affectedPackageWithDecorations{ makeAffectedPackageWithFixState(v6.FixedStatus), {AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: nil}}, }, fixedStates: []string{"fixed"}, expectedLen: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterByFixedStateForPackages(tt.packages, tt.fixedStates) assert.Equal(t, tt.expectedLen, len(result)) }) } } func TestFilterByFixedStateForCPEs(t *testing.T) { tests := []struct { name string cpes []affectedCPEWithDecorations fixedStates []string expectedLen int }{ { name: "empty fixed states returns all CPEs", cpes: []affectedCPEWithDecorations{ {AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: &v6.PackageBlob{}}}, {AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: &v6.PackageBlob{}}}, }, fixedStates: []string{}, expectedLen: 2, }, { name: "filter by fixed state", cpes: []affectedCPEWithDecorations{ makeAffectedCPEWithFixState(v6.FixedStatus), makeAffectedCPEWithFixState(v6.NotFixedStatus), }, fixedStates: []string{"fixed"}, expectedLen: 1, }, { name: "filter by multiple states", cpes: []affectedCPEWithDecorations{ makeAffectedCPEWithFixState(v6.FixedStatus), makeAffectedCPEWithFixState(v6.NotFixedStatus), makeAffectedCPEWithFixState(v6.WontFixStatus), }, fixedStates: []string{"not-fixed", "wont-fix"}, expectedLen: 2, }, { name: "CPEs with nil blob are filtered out", cpes: []affectedCPEWithDecorations{ makeAffectedCPEWithFixState(v6.FixedStatus), {AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: nil}}, }, fixedStates: []string{"fixed"}, expectedLen: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterByFixedStateForCPEs(tt.cpes, tt.fixedStates) assert.Equal(t, tt.expectedLen, len(result)) }) } } func makeAffectedPackageWithFixState(state v6.FixStatus) affectedPackageWithDecorations { return affectedPackageWithDecorations{ AffectedPackageHandle: v6.AffectedPackageHandle{ BlobValue: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: state, Version: "1.0.0", }, }, }, }, }, } } func makeAffectedCPEWithFixState(state v6.FixStatus) affectedCPEWithDecorations { return affectedCPEWithDecorations{ AffectedCPEHandle: v6.AffectedCPEHandle{ BlobValue: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: state, Version: "1.0.0", }, }, }, }, }, } } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/common.go ================================================ package dbsearch import v6 "github.com/anchore/grype/grype/db/v6" const ( fixStateFixed = "fixed" fixStateNotFixed = "not-fixed" fixStateWontFix = "wont-fix" fixStateUnknown = "unknown" ) // getFixStateFromPackageBlob determines the overall fix state for a package blob. // When multiple ranges exist with different fix states, precedence is applied: // fixed > wont-fix > not-fixed > unknown // This ensures that if ANY range has a fix available, the package is considered fixable. func getFixStateFromPackageBlob(blob *v6.PackageBlob) string { if blob == nil { return fixStateUnknown } hasFixed := false hasNotFixed := false hasWontFix := false for _, r := range blob.Ranges { if r.Fix == nil { continue } switch r.Fix.State { case v6.FixedStatus: hasFixed = true case v6.WontFixStatus: hasWontFix = true case v6.NotFixedStatus: hasNotFixed = true } } if hasFixed { return fixStateFixed } if hasWontFix { return fixStateWontFix } if hasNotFixed { return fixStateNotFixed } return fixStateUnknown } func filterByFixedStateForPackages(packages []affectedPackageWithDecorations, fixedStates []string) []affectedPackageWithDecorations { if len(fixedStates) == 0 { return packages } stateSet := make(map[string]bool) for _, state := range fixedStates { stateSet[state] = true } var filtered []affectedPackageWithDecorations for _, pkg := range packages { if pkg.BlobValue == nil { continue } fixState := getFixStateFromPackageBlob(pkg.BlobValue) if stateSet[fixState] { filtered = append(filtered, pkg) } } return filtered } func filterByFixedStateForCPEs(cpes []affectedCPEWithDecorations, fixedStates []string) []affectedCPEWithDecorations { if len(fixedStates) == 0 { return cpes } stateSet := make(map[string]bool) for _, state := range fixedStates { stateSet[state] = true } var filtered []affectedCPEWithDecorations for _, cpe := range cpes { if cpe.BlobValue == nil { continue } fixState := getFixStateFromPackageBlob(cpe.BlobValue) if stateSet[fixState] { filtered = append(filtered, cpe) } } return filtered } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/matches.go ================================================ package dbsearch import ( "errors" "fmt" "sort" "github.com/hashicorp/go-multierror" v6 "github.com/anchore/grype/grype/db/v6" ) // Matches is the JSON document for the `db search` command type Matches []Match // Match represents a pairing of a vulnerability advisory with the packages affected by the vulnerability. type Match struct { // Vulnerability is the core advisory record for a single known vulnerability from a specific provider. Vulnerability VulnerabilityInfo `json:"vulnerability"` // AffectedPackages is the list of packages affected by the vulnerability. AffectedPackages []AffectedPackageInfo `json:"packages"` } func (m Match) Flatten() []AffectedPackage { var rows []AffectedPackage for _, pkg := range m.AffectedPackages { rows = append(rows, AffectedPackage{ Vulnerability: m.Vulnerability, AffectedPackageInfo: pkg, }) } return rows } func (m Matches) Flatten() []AffectedPackage { var rows []AffectedPackage for _, r := range m { rows = append(rows, r.Flatten()...) } return rows } func newMatchesRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []Match, retErr error) { // nolint:funlen var affectedPkgsByVuln = make(map[v6.ID][]AffectedPackageInfo) var vulnsByID = make(map[v6.ID]v6.VulnerabilityHandle) var decorationsByID = make(map[v6.ID]vulnerabilityDecorations) for i := range affectedPkgs { pkg := affectedPkgs[i] var detail v6.PackageBlob if pkg.BlobValue != nil { detail = *pkg.BlobValue } if pkg.Vulnerability == nil { retErr = multierror.Append(retErr, fmt.Errorf("affected package record missing vulnerability: %+v", pkg)) continue } if _, ok := vulnsByID[pkg.Vulnerability.ID]; !ok { vulnsByID[pkg.Vulnerability.ID] = *pkg.Vulnerability decorationsByID[pkg.Vulnerability.ID] = pkg.vulnerabilityDecorations } aff := AffectedPackageInfo{ Model: &pkg.AffectedPackageHandle, OS: toOS(pkg.OperatingSystem), Package: toPackage(pkg.Package), Namespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle), Detail: detail, } affectedPkgsByVuln[pkg.Vulnerability.ID] = append(affectedPkgsByVuln[pkg.Vulnerability.ID], aff) } for _, ac := range affectedCPEs { var detail v6.PackageBlob if ac.BlobValue != nil { detail = *ac.BlobValue } if ac.Vulnerability == nil { retErr = multierror.Append(retErr, fmt.Errorf("affected CPE record missing vulnerability: %+v", ac)) continue } var c *CPE if ac.CPE != nil { cv := CPE(*ac.CPE) c = &cv } if _, ok := vulnsByID[ac.Vulnerability.ID]; !ok { vulnsByID[ac.Vulnerability.ID] = *ac.Vulnerability decorationsByID[ac.Vulnerability.ID] = ac.vulnerabilityDecorations } aff := AffectedPackageInfo{ // tracking model information is not possible with CPE handles CPE: c, Namespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD Detail: detail, } affectedPkgsByVuln[ac.Vulnerability.ID] = append(affectedPkgsByVuln[ac.Vulnerability.ID], aff) } for vulnID, vuln := range vulnsByID { rows = append(rows, Match{ Vulnerability: newVulnerabilityInfo(vuln, decorationsByID[vulnID]), AffectedPackages: affectedPkgsByVuln[vulnID], }) } sort.Slice(rows, func(i, j int) bool { return rows[i].Vulnerability.ID < rows[j].Vulnerability.ID }) return rows, retErr } func FindMatches(reader interface { v6.AffectedPackageStoreReader v6.AffectedCPEStoreReader v6.VulnerabilityDecoratorStoreReader }, criteria AffectedPackagesOptions) (Matches, error) { allAffectedPkgs, allAffectedCPEs, fetchErr := findAffectedPackages(reader, criteria) if fetchErr != nil { if !errors.Is(fetchErr, v6.ErrLimitReached) { return nil, fetchErr } } if len(criteria.FixedStates) > 0 { allAffectedPkgs = filterByFixedStateForPackages(allAffectedPkgs, criteria.FixedStates) allAffectedCPEs = filterByFixedStateForCPEs(allAffectedCPEs, criteria.FixedStates) } rows, presErr := newMatchesRows(allAffectedPkgs, allAffectedCPEs) if presErr != nil { return nil, presErr } return rows, fetchErr } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/matches_test.go ================================================ package dbsearch import ( "testing" "github.com/stretchr/testify/assert" v6 "github.com/anchore/grype/grype/db/v6" ) func TestGetFixStateFromBlob(t *testing.T) { tests := []struct { name string blob *v6.PackageBlob expected string }{ { name: "nil blob returns unknown", blob: nil, expected: "unknown", }, { name: "empty blob returns unknown", blob: &v6.PackageBlob{}, expected: "unknown", }, { name: "blob with fixed status", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.FixedStatus, Version: "1.2.3", }, }, }, }, expected: "fixed", }, { name: "blob with not-fixed status", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.NotFixedStatus, }, }, }, }, expected: "not-fixed", }, { name: "blob with wont-fix status", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.WontFixStatus, }, }, }, }, expected: "wont-fix", }, { name: "blob with no fix returns unknown", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: nil, }, }, }, expected: "unknown", }, { name: "blob with mixed statuses prefers fixed", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.NotFixedStatus, }, }, { Fix: &v6.Fix{ State: v6.FixedStatus, Version: "2.0.0", }, }, }, }, expected: "fixed", }, { name: "blob with wont-fix and not-fixed prefers wont-fix", blob: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: v6.NotFixedStatus, }, }, { Fix: &v6.Fix{ State: v6.WontFixStatus, }, }, }, }, expected: "wont-fix", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getFixStateFromPackageBlob(tt.blob) assert.Equal(t, tt.expected, result) }) } } func TestFilterByFixedState(t *testing.T) { tests := []struct { name string packages []affectedPackageWithDecorations fixedStates []string expectedLen int expectedStrs []string }{ { name: "empty fixed states returns all packages", packages: makeTestPackages(3), fixedStates: []string{}, expectedLen: 3, expectedStrs: nil, }, { name: "filter by fixed state", packages: []affectedPackageWithDecorations{ makePackageWithFixState(v6.FixedStatus), makePackageWithFixState(v6.NotFixedStatus), makePackageWithFixState(v6.WontFixStatus), }, fixedStates: []string{"fixed"}, expectedLen: 1, expectedStrs: []string{"fixed"}, }, { name: "filter by multiple states", packages: []affectedPackageWithDecorations{ makePackageWithFixState(v6.FixedStatus), makePackageWithFixState(v6.NotFixedStatus), makePackageWithFixState(v6.WontFixStatus), }, fixedStates: []string{"fixed", "wont-fix"}, expectedLen: 2, expectedStrs: []string{"fixed", "wont-fix"}, }, { name: "filter with no matches", packages: []affectedPackageWithDecorations{ makePackageWithFixState(v6.NotFixedStatus), makePackageWithFixState(v6.WontFixStatus), }, fixedStates: []string{"fixed"}, expectedLen: 0, expectedStrs: nil, }, { name: "packages with nil blob are filtered out", packages: []affectedPackageWithDecorations{ makePackageWithFixState(v6.FixedStatus), {AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: nil}}, }, fixedStates: []string{"fixed", "unknown"}, expectedLen: 1, expectedStrs: []string{"fixed"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterByFixedStateForPackages(tt.packages, tt.fixedStates) assert.Equal(t, tt.expectedLen, len(result)) if tt.expectedStrs != nil { var resultStates []string for _, pkg := range result { resultStates = append(resultStates, getFixStateFromPackageBlob(pkg.BlobValue)) } assert.ElementsMatch(t, tt.expectedStrs, resultStates) } }) } } func TestFilterCPEsByFixedState(t *testing.T) { tests := []struct { name string cpes []affectedCPEWithDecorations fixedStates []string expectedLen int expectedStrs []string }{ { name: "empty fixed states returns all CPEs", cpes: makeTestCPEs(3), fixedStates: []string{}, expectedLen: 3, expectedStrs: nil, }, { name: "filter by fixed state", cpes: []affectedCPEWithDecorations{ makeCPEWithFixState(v6.FixedStatus), makeCPEWithFixState(v6.NotFixedStatus), makeCPEWithFixState(v6.WontFixStatus), }, fixedStates: []string{"fixed"}, expectedLen: 1, expectedStrs: []string{"fixed"}, }, { name: "filter by multiple states", cpes: []affectedCPEWithDecorations{ makeCPEWithFixState(v6.FixedStatus), makeCPEWithFixState(v6.NotFixedStatus), makeCPEWithFixState(v6.WontFixStatus), }, fixedStates: []string{"not-fixed", "wont-fix"}, expectedLen: 2, expectedStrs: []string{"not-fixed", "wont-fix"}, }, { name: "CPEs with nil blob are filtered out", cpes: []affectedCPEWithDecorations{ makeCPEWithFixState(v6.FixedStatus), {AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: nil}}, }, fixedStates: []string{"fixed", "unknown"}, expectedLen: 1, expectedStrs: []string{"fixed"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterByFixedStateForCPEs(tt.cpes, tt.fixedStates) assert.Equal(t, tt.expectedLen, len(result)) if tt.expectedStrs != nil { var resultStates []string for _, cpe := range result { resultStates = append(resultStates, getFixStateFromPackageBlob(cpe.BlobValue)) } assert.ElementsMatch(t, tt.expectedStrs, resultStates) } }) } } func makeTestPackages(count int) []affectedPackageWithDecorations { packages := make([]affectedPackageWithDecorations, count) for i := 0; i < count; i++ { packages[i] = affectedPackageWithDecorations{ AffectedPackageHandle: v6.AffectedPackageHandle{ BlobValue: &v6.PackageBlob{}, }, } } return packages } func makePackageWithFixState(state v6.FixStatus) affectedPackageWithDecorations { return affectedPackageWithDecorations{ AffectedPackageHandle: v6.AffectedPackageHandle{ BlobValue: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: state, Version: "1.0.0", }, }, }, }, }, } } func makeTestCPEs(count int) []affectedCPEWithDecorations { cpes := make([]affectedCPEWithDecorations, count) for i := 0; i < count; i++ { cpes[i] = affectedCPEWithDecorations{ AffectedCPEHandle: v6.AffectedCPEHandle{ BlobValue: &v6.PackageBlob{}, }, } } return cpes } func makeCPEWithFixState(state v6.FixStatus) affectedCPEWithDecorations { return affectedCPEWithDecorations{ AffectedCPEHandle: v6.AffectedCPEHandle{ BlobValue: &v6.PackageBlob{ Ranges: []v6.Range{ { Fix: &v6.Fix{ State: state, Version: "1.0.0", }, }, }, }, }, } } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/versions.go ================================================ package dbsearch const ( // MatchesSchemaVersion is the schema version for the `db search` command MatchesSchemaVersion = "1.1.3" // MatchesSchemaVersion Changelog: // 1.0.0 - Initial schema 🎉 // 1.0.1 - Add KEV and EPSS data to vulnerability matches // 1.0.2 - Add v5 namespace emulation for affected packages // 1.0.3 - Add severity string field to vulnerability object // 1.1.0 - Add fix available date information to vulnerability range object. This removes existing unused git-commit and date fields from the schema, but is a non-breaking change. // 1.1.1 - Add unaffected package and unaffected cpe to output // 1.1.2 - Add CWE IDs to vulnerability output // 1.1.3 - Add ID field to Reference (for advisory IDs like RHSA-2023:5455) // VulnerabilitiesSchemaVersion is the schema version for the `db search vuln` command VulnerabilitiesSchemaVersion = "1.0.5" // VulnerabilitiesSchemaVersion // 1.0.0 - Initial schema 🎉 // 1.0.1 - Add KEV and EPSS data to vulnerability // 1.0.3 - Add severity string field to vulnerability object // 1.0.4 - Add CWE IDs to vulnerability output // 1.0.5 - Add ID field to Reference (for advisory IDs like RHSA-2023:5455) ) ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go ================================================ package dbsearch import ( "errors" "fmt" "sort" "time" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/cvss" "github.com/anchore/grype/internal/log" ) // Vulnerabilities is the JSON document for the `db search vuln` command type Vulnerabilities []Vulnerability // Vulnerability represents the core advisory record for a single known vulnerability from a specific provider. type Vulnerability struct { VulnerabilityInfo `json:",inline"` // OperatingSystems is a list of operating systems affected by the vulnerability OperatingSystems []OperatingSystem `json:"operating_systems"` // AffectedPackages is the number of packages affected by the vulnerability AffectedPackages int `json:"affected_packages"` } type VulnerabilityInfo struct { // TODO: remove this when namespace is no longer used Model v6.VulnerabilityHandle `json:"-"` // tracking package handle info is necessary for namespace lookup v6.VulnerabilityBlob `json:",inline"` // Severity is the single string representation of the vulnerability's severity based on the set of available severity values Severity string `json:"severity,omitempty"` // Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider // should be scoped to a specific vulnerability dataset, for instance, the "ubuntu" provider for all records from // Canonicals' Ubuntu Security Notices (for all Ubuntu distro versions). Provider string `json:"provider"` // Status conveys the actionability of the current record (one of "active", "analyzing", "rejected", "disputed") Status string `json:"status"` // PublishedDate is the date the vulnerability record was first published PublishedDate *time.Time `json:"published_date,omitempty"` // ModifiedDate is the date the vulnerability record was last modified ModifiedDate *time.Time `json:"modified_date,omitempty"` // WithdrawnDate is the date the vulnerability record was withdrawn WithdrawnDate *time.Time `json:"withdrawn_date,omitempty"` // KnownExploited is a list of known exploited vulnerabilities from the CISA KEV dataset KnownExploited []KnownExploited `json:"known_exploited,omitempty"` // EPSS is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability EPSS []EPSS `json:"epss,omitempty"` // CWEs is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability CWEs []CWE `json:"cwes,omitempty"` } // OperatingSystem represents specific release of an operating system. type OperatingSystem struct { // Name is the operating system family name (e.g. "debian") Name string `json:"name"` // Version is the semver-ish or codename for the release of the operating system Version string `json:"version"` } type KnownExploited struct { CVE string `json:"cve"` VendorProject string `json:"vendor_project,omitempty"` Product string `json:"product,omitempty"` DateAdded string `json:"date_added,omitempty"` RequiredAction string `json:"required_action,omitempty"` DueDate string `json:"due_date,omitempty"` KnownRansomwareCampaignUse string `json:"known_ransomware_campaign_use"` Notes string `json:"notes,omitempty"` URLs []string `json:"urls,omitempty"` CWEs []string `json:"cwes,omitempty"` } type EPSS struct { CVE string `json:"cve"` EPSS float64 `json:"epss"` Percentile float64 `json:"percentile"` Date string `json:"date"` } type CWE struct { Cve string `json:"cve"` CWE string `json:"cwe"` Source string `json:"source"` Type string `json:"type"` } type CVSSSeverity struct { // Vector is the CVSS assessment as a parameterized string Vector string `json:"vector"` // Version is the CVSS version (e.g. "3.0") Version string `json:"version,omitempty"` // Metrics is the CVSS quantitative assessment based on the vector Metrics CvssMetrics `json:"metrics"` } type CvssMetrics struct { BaseScore float64 `json:"baseScore"` ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` ImpactScore *float64 `json:"impactScore,omitempty"` } type vulnerabilityAffectedPackageJoin struct { Vulnerability v6.VulnerabilityHandle OperatingSystems []v6.OperatingSystem AffectedPackages int vulnerabilityDecorations } type VulnerabilitiesOptions struct { Vulnerability v6.VulnerabilitySpecifiers RecordLimit int } func newVulnerabilityRows(vaps ...vulnerabilityAffectedPackageJoin) (rows []Vulnerability) { for _, vap := range vaps { rows = append(rows, Vulnerability{ VulnerabilityInfo: newVulnerabilityInfo(vap.Vulnerability, vap.vulnerabilityDecorations), OperatingSystems: newOperatingSystems(vap.OperatingSystems), AffectedPackages: vap.AffectedPackages, }) } return rows } func newVulnerabilityInfo(vuln v6.VulnerabilityHandle, vc vulnerabilityDecorations) VulnerabilityInfo { var blob v6.VulnerabilityBlob if vuln.BlobValue != nil { blob = *vuln.BlobValue } patchCVSSMetrics(&blob) return VulnerabilityInfo{ Model: vuln, VulnerabilityBlob: blob, Severity: getSeverity(blob.Severities), Provider: vuln.Provider.ID, Status: string(vuln.Status), PublishedDate: vuln.PublishedDate, ModifiedDate: vuln.ModifiedDate, WithdrawnDate: vuln.WithdrawnDate, KnownExploited: vc.KnownExploited, EPSS: vc.EPSS, } } func patchCVSSMetrics(blob *v6.VulnerabilityBlob) { for i := range blob.Severities { sev := &blob.Severities[i] if val, ok := sev.Value.(v6.CVSSSeverity); ok { met, err := cvss.ParseMetricsFromVector(val.Vector) if err != nil { log.WithFields("vector", val.Vector, "error", err).Debug("unable to parse CVSS vector") continue } newSev := CVSSSeverity{ Vector: val.Vector, Version: val.Version, Metrics: CvssMetrics{ BaseScore: met.BaseScore, ExploitabilityScore: met.ExploitabilityScore, ImpactScore: met.ImpactScore, }, } sev.Value = newSev } } } func newOperatingSystems(oss []v6.OperatingSystem) (os []OperatingSystem) { for _, o := range oss { os = append(os, OperatingSystem{ Name: o.Name, Version: o.Version(), }) } return os } func FindVulnerabilities(reader interface { //nolint:funlen v6.VulnerabilityStoreReader v6.AffectedPackageStoreReader v6.VulnerabilityDecoratorStoreReader }, config VulnerabilitiesOptions, ) ([]Vulnerability, error) { log.WithFields("vulnSpecs", len(config.Vulnerability)).Debug("fetching vulnerabilities") if config.RecordLimit == 0 { log.Warn("no record limit set! For queries with large result sets this may result in performance issues") } var vulns []v6.VulnerabilityHandle var limitReached bool for _, vulnSpec := range config.Vulnerability { vs, err := reader.GetVulnerabilities(&vulnSpec, &v6.GetVulnerabilityOptions{ Preload: true, Limit: config.RecordLimit, }) if err != nil { if !errors.Is(err, v6.ErrLimitReached) { return nil, fmt.Errorf("unable to get vulnerabilities: %w", err) } limitReached = true break } vulns = append(vulns, vs...) } log.WithFields("vulns", len(vulns)).Debug("fetching affected packages") // find all affected packages for this vulnerability, so we can gather os information var pairs []vulnerabilityAffectedPackageJoin for _, vuln := range vulns { affected, fetchErr := reader.GetAffectedPackages(nil, &v6.GetPackageOptions{ PreloadOS: true, Vulnerabilities: []v6.VulnerabilitySpecifier{ { ID: vuln.ID, }, }, Limit: config.RecordLimit, }) if fetchErr != nil { if !errors.Is(fetchErr, v6.ErrLimitReached) { return nil, fmt.Errorf("unable to get affected packages: %w", fetchErr) } limitReached = true } distros := make(map[v6.ID]v6.OperatingSystem) for _, a := range affected { if a.OperatingSystem != nil { if _, ok := distros[a.OperatingSystem.ID]; !ok { distros[a.OperatingSystem.ID] = *a.OperatingSystem } } } var distrosSlice []v6.OperatingSystem for _, d := range distros { distrosSlice = append(distrosSlice, d) } sort.Slice(distrosSlice, func(i, j int) bool { return distrosSlice[i].ID < distrosSlice[j].ID }) pairs = append(pairs, vulnerabilityAffectedPackageJoin{ Vulnerability: vuln, OperatingSystems: distrosSlice, AffectedPackages: len(affected), }) if errors.Is(fetchErr, v6.ErrLimitReached) { break } } for i := range pairs { decorateVulnerabilities(reader, &pairs[i]) } var err error if limitReached { err = v6.ErrLimitReached } return newVulnerabilityRows(pairs...), err } func getSeverity(sevs []v6.Severity) string { if len(sevs) == 0 { return vulnerability.UnknownSeverity.String() } // get the first severity value (which is ranked highest) switch v := sevs[0].Value.(type) { case string: return v case CVSSSeverity: return cvss.SeverityFromBaseScore(v.Metrics.BaseScore).String() } return fmt.Sprintf("%v", sevs[0].Value) } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go ================================================ package dbsearch import ( "fmt" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/vulnerability" ) func TestGetSeverity(t *testing.T) { tests := []struct { name string input []v6.Severity expected string }{ { name: "empty list", input: []v6.Severity{}, expected: vulnerability.UnknownSeverity.String(), }, { name: "string severity", input: []v6.Severity{ { Scheme: "HML", Value: "high", Source: "nvd@nist.gov", Rank: 1, }, }, expected: "high", }, { name: "CVSS severity", input: []v6.Severity{ { Scheme: "CVSS_V3", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 9.8, }, }, Source: "nvd@nist.gov", Rank: 1, }, }, expected: "critical", }, { name: "other value type", input: []v6.Severity{ { Scheme: "OTHER", Value: 42.0, Source: "custom", Rank: 1, }, }, expected: "42", }, { name: "multiple severities", input: []v6.Severity{ { Scheme: "HML", Value: "high", Source: "nvd@nist.gov", Rank: 1, }, { Scheme: "CVSS_V3", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 9.8, }, }, Source: "nvd@nist.gov", Rank: 2, }, }, expected: "high", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := getSeverity(tt.input) require.Equal(t, tt.expected, actual) }) } } func TestNewVulnerabilityRows(t *testing.T) { vap := vulnerabilityAffectedPackageJoin{ Vulnerability: v6.VulnerabilityHandle{ ID: 1, Name: "CVE-1234-5678", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), WithdrawnDate: nil, Provider: &v6.Provider{ID: "provider1"}, BlobValue: &v6.VulnerabilityBlob{ Description: "Test description", Severities: []v6.Severity{ { Scheme: "CVSS_V3", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 9.8, }, }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, OperatingSystems: []v6.OperatingSystem{ {Name: "Linux", MajorVersion: "5", MinorVersion: "10"}, }, AffectedPackages: 5, vulnerabilityDecorations: vulnerabilityDecorations{ KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, } rows := newVulnerabilityRows(vap) expected := []Vulnerability{ { VulnerabilityInfo: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{ Description: "Test description", Severities: []v6.Severity{ { Scheme: "CVSS_V3", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 9.8, }, }, Source: "nvd@nist.gov", Rank: 1, }, }, }, Severity: "critical", Provider: "provider1", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), WithdrawnDate: nil, KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, OperatingSystems: []OperatingSystem{ {Name: "Linux", Version: "5.10"}, }, AffectedPackages: 5, }, } if diff := cmp.Diff(expected, rows, cmpOpts()...); diff != "" { t.Errorf("unexpected rows (-want +got):\n%s", diff) } } func TestVulnerabilities(t *testing.T) { mockReader := new(mockVulnReader) vulnSpecs := v6.VulnerabilitySpecifiers{ {Name: "CVE-1234-5678"}, } mockReader.On("GetVulnerabilities", mock.Anything, mock.Anything).Return([]v6.VulnerabilityHandle{ { ID: 1, Name: "CVE-1234-5678", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), Provider: &v6.Provider{ID: "provider1"}, BlobValue: &v6.VulnerabilityBlob{ Description: "Test description", Severities: []v6.Severity{ { Scheme: v6.SeveritySchemeCVSS, Value: v6.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", Version: "3.1", }, Source: "nvd", Rank: 1, }, }, }, }, }, nil) mockReader.On("GetAffectedPackages", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{ { OperatingSystem: &v6.OperatingSystem{Name: "Linux", MajorVersion: "5", MinorVersion: "10"}, }, }, nil) mockReader.On("GetKnownExploitedVulnerabilities", "CVE-1234-5678").Return([]v6.KnownExploitedVulnerabilityHandle{ { Cve: "CVE-1234-5678", BlobValue: &v6.KnownExploitedVulnerabilityBlob{ Cve: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)), RequiredAction: "Yes", DueDate: ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)), KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, }, nil) mockReader.On("GetEpss", "CVE-1234-5678").Return([]v6.EpssHandle{ { Cve: "CVE-1234-5678", Epss: 0.893, Percentile: 0.99, Date: time.Date(2025, 2, 24, 0, 0, 0, 0, time.UTC), }, }, nil) results, err := FindVulnerabilities(mockReader, VulnerabilitiesOptions{Vulnerability: vulnSpecs}) require.NoError(t, err) expected := []Vulnerability{ { VulnerabilityInfo: VulnerabilityInfo{ VulnerabilityBlob: v6.VulnerabilityBlob{ Description: "Test description", Severities: []v6.Severity{ { Scheme: "CVSS", Value: CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", Version: "3.1", Metrics: CvssMetrics{ BaseScore: 7.5, ExploitabilityScore: ptr(3.9), ImpactScore: ptr(3.6), }, }, Source: "nvd", Rank: 1, }, }, }, Severity: "high", Provider: "provider1", Status: "active", PublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), ModifiedDate: ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)), WithdrawnDate: nil, KnownExploited: []KnownExploited{ { CVE: "CVE-1234-5678", VendorProject: "LinuxFoundation", Product: "Linux", DateAdded: "2025-02-02", RequiredAction: "Yes", DueDate: "2025-02-02", KnownRansomwareCampaignUse: "Known", Notes: "note!", URLs: []string{"https://example.com"}, CWEs: []string{"CWE-1234"}, }, }, EPSS: []EPSS{ { CVE: "CVE-1234-5678", EPSS: 0.893, Percentile: 0.99, Date: "2025-02-24", }, }, }, OperatingSystems: []OperatingSystem{ {Name: "Linux", Version: "5.10"}, }, AffectedPackages: 1, }, } if diff := cmp.Diff(expected, results, cmpOpts()...); diff != "" { t.Errorf("unexpected results (-want +got):\n%s", diff) } } func TestFindVulnerabilities_DecorationErrors(t *testing.T) { tests := []struct { name string kevErr error epssErr error }{ { name: "EPSS error is not fatal", epssErr: fmt.Errorf("unable to fetch EPSS metadata: record not found"), }, { name: "KEV error is not fatal", kevErr: fmt.Errorf("unable to fetch KEV records: record not found"), }, { name: "both EPSS and KEV errors are not fatal", kevErr: fmt.Errorf("unable to fetch KEV records: record not found"), epssErr: fmt.Errorf("unable to fetch EPSS metadata: record not found"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockReader := new(mockVulnReader) mockReader.On("GetVulnerabilities", mock.Anything, mock.Anything).Return([]v6.VulnerabilityHandle{ { ID: 1, Name: "CVE-2021-22947", Status: "active", Provider: &v6.Provider{ID: "nvd"}, BlobValue: &v6.VulnerabilityBlob{ Description: "Test vuln", }, }, }, nil) mockReader.On("GetAffectedPackages", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{}, nil) mockReader.On("GetKnownExploitedVulnerabilities", "CVE-2021-22947").Return( []v6.KnownExploitedVulnerabilityHandle{}, tt.kevErr, ) mockReader.On("GetEpss", "CVE-2021-22947").Return( []v6.EpssHandle{}, tt.epssErr, ) results, err := FindVulnerabilities(mockReader, VulnerabilitiesOptions{ Vulnerability: v6.VulnerabilitySpecifiers{{Name: "CVE-2021-22947"}}, }) require.NoError(t, err, "decoration errors should not propagate as fatal errors") require.Len(t, results, 1) require.Equal(t, "Test vuln", results[0].VulnerabilityBlob.Description) require.Empty(t, results[0].KnownExploited) require.Empty(t, results[0].EPSS) }) } } type mockVulnReader struct { mock.Mock } func (m *mockVulnReader) GetVulnerabilities(vuln *v6.VulnerabilitySpecifier, config *v6.GetVulnerabilityOptions) ([]v6.VulnerabilityHandle, error) { args := m.Called(vuln, config) return args.Get(0).([]v6.VulnerabilityHandle), args.Error(1) } func (m *mockVulnReader) GetAffectedPackages(pkg *v6.PackageSpecifier, config *v6.GetPackageOptions) ([]v6.AffectedPackageHandle, error) { args := m.Called(pkg, config) return args.Get(0).([]v6.AffectedPackageHandle), args.Error(1) } func (m *mockVulnReader) GetKnownExploitedVulnerabilities(cve string) ([]v6.KnownExploitedVulnerabilityHandle, error) { args := m.Called(cve) return args.Get(0).([]v6.KnownExploitedVulnerabilityHandle), args.Error(1) } func (m *mockVulnReader) GetEpss(cve string) ([]v6.EpssHandle, error) { args := m.Called(cve) return args.Get(0).([]v6.EpssHandle), args.Error(1) } func (m *mockVulnReader) GetCWEs(cve string) ([]v6.CWEHandle, error) { args := m.Called(cve) return args.Get(0).([]v6.CWEHandle), args.Error(1) } func cmpOpts() []cmp.Option { return []cmp.Option{ cmpopts.IgnoreFields(AffectedPackageInfo{}, "Model"), cmpopts.IgnoreFields(VulnerabilityInfo{}, "Model"), } } ================================================ FILE: cmd/grype/cli/commands/internal/dbsearch/vulnerability_decorations.go ================================================ package dbsearch import ( "strings" "time" "github.com/hashicorp/go-multierror" "github.com/scylladb/go-set/strset" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/log" ) type canonicalVulnerability interface { getCVEs() []string decorate(kevs []KnownExploited, epss []EPSS) } // vulnerabilityDecorations are separate model elements (not from VulnerabilityHandle) that is fetched on // the provider.GetMetadata() path instead of the provider.GetVulnerabilities() path. I hope for these two paths // to be merged into the same path come grype 1.0, in which case these elements would already be on the // store get methods when crafting a vulnerability. type vulnerabilityDecorations struct { KnownExploited []KnownExploited `json:"knownExploited,omitempty"` EPSS []EPSS `json:"epss,omitempty"` } func decorateVulnerabilities(reader v6.VulnerabilityDecoratorStoreReader, cvs ...canonicalVulnerability) { for _, cv := range cvs { cves := cv.getCVEs() if len(cves) == 0 { continue } knownExploited, err := fetchKnownExploited(reader, cves) if err != nil { log.WithFields("error", err).Debug("unable to get known exploited vulnerabilities") } epss, err := fetchEpss(reader, cves) if err != nil { log.WithFields("error", err).Debug("unable to get EPSS scores") } cv.decorate(knownExploited, epss) } } func (afj *vulnerabilityAffectedPackageJoin) getCVEs() []string { if afj == nil { return nil } return getCVEs(&afj.Vulnerability) } func getCVEs(v *v6.VulnerabilityHandle) []string { var cves []string set := strset.New() addCVE := func(id string) { lower := strings.ToLower(id) if strings.HasPrefix(lower, "cve-") { if !set.Has(lower) { cves = append(cves, id) set.Add(lower) } } } if v == nil { return cves } addCVE(v.Name) if v.BlobValue == nil { return cves } addCVE(v.BlobValue.ID) for _, alias := range v.BlobValue.Aliases { addCVE(alias) } return cves } func (vd *vulnerabilityDecorations) decorate(kevs []KnownExploited, epss []EPSS) { if vd == nil { return } vd.KnownExploited = kevs vd.EPSS = epss } func fetchKnownExploited(reader v6.VulnerabilityDecoratorStoreReader, cves []string) ([]KnownExploited, error) { var out []KnownExploited var errs error for _, cve := range cves { kevs, err := reader.GetKnownExploitedVulnerabilities(cve) if err != nil { errs = multierror.Append(errs, err) continue } for _, kev := range kevs { out = append(out, KnownExploited{ CVE: kev.Cve, VendorProject: kev.BlobValue.VendorProject, Product: kev.BlobValue.Product, DateAdded: kev.BlobValue.DateAdded.Format(time.DateOnly), RequiredAction: kev.BlobValue.RequiredAction, DueDate: kev.BlobValue.DueDate.Format(time.DateOnly), KnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse, Notes: kev.BlobValue.Notes, URLs: kev.BlobValue.URLs, CWEs: kev.BlobValue.CWEs, }) } } return out, errs } func fetchEpss(reader v6.VulnerabilityDecoratorStoreReader, cves []string) ([]EPSS, error) { var out []EPSS var errs error for _, cve := range cves { entries, err := reader.GetEpss(cve) if err != nil { errs = multierror.Append(errs, err) continue } for _, entry := range entries { out = append(out, EPSS{ CVE: entry.Cve, EPSS: entry.Epss, Percentile: entry.Percentile, Date: entry.Date.Format(time.DateOnly), }) } } return out, errs } ================================================ FILE: cmd/grype/cli/commands/internal/jsonschema/main.go ================================================ package main import ( "bytes" "encoding/json" "fmt" "go/ast" "io" "os" "os/exec" "path/filepath" "reflect" "strings" "github.com/invopop/jsonschema" "golang.org/x/tools/go/packages" "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" ) func main() { pkgPatterns := []string{"../dbsearch", "../../../../../../grype/db/v6"} comments := parseCommentsFromPackages(pkgPatterns) fmt.Printf("Extracted field comments from %d structs\n", len(comments)) compose(dbsearch.Matches{}, "db-search", dbsearch.MatchesSchemaVersion, comments) compose(dbsearch.Vulnerabilities{}, "db-search-vuln", dbsearch.VulnerabilitiesSchemaVersion, comments) } func compose(document any, component, version string, comments map[string]map[string]string) { write(encode(build(document, component, version, comments)), component, version) } func write(schema []byte, component, version string) { parent := filepath.Join(repoRoot(), "schema", "grype", component, "json") schemaPath := filepath.Join(parent, fmt.Sprintf("schema-%s.json", version)) latestSchemaPath := filepath.Join(parent, "schema-latest.json") if _, err := os.Stat(schemaPath); !os.IsNotExist(err) { // check if the schema is the same... existingFh, err := os.Open(schemaPath) if err != nil { panic(err) } existingSchemaBytes, err := io.ReadAll(existingFh) if err != nil { panic(err) } if bytes.Equal(existingSchemaBytes, schema) { // the generated schema is the same, bail with no error :) fmt.Printf("No change to the existing %q schema!\n", component) return } // the generated schema is different, bail with error :( fmt.Printf("Cowardly refusing to overwrite existing %q schema (%s)!\nSee the README.md for how to increment\n", component, schemaPath) os.Exit(1) } fh, err := os.Create(schemaPath) if err != nil { panic(err) } defer fh.Close() _, err = fh.Write(schema) if err != nil { panic(err) } latestFile, err := os.Create(latestSchemaPath) if err != nil { panic(err) } defer latestFile.Close() _, err = latestFile.Write(schema) if err != nil { panic(err) } fmt.Printf("Wrote new %q schema to %q\n", component, schemaPath) } func encode(schema *jsonschema.Schema) []byte { newSchemaBuffer := new(bytes.Buffer) enc := json.NewEncoder(newSchemaBuffer) // prevent > and < from being escaped in the payload enc.SetEscapeHTML(false) enc.SetIndent("", " ") err := enc.Encode(&schema) if err != nil { panic(err) } return newSchemaBuffer.Bytes() } func build(document any, component, version string, comments map[string]map[string]string) *jsonschema.Schema { reflector := &jsonschema.Reflector{ BaseSchemaID: schemaID(component, version), AllowAdditionalProperties: true, Namer: func(r reflect.Type) string { return strings.TrimPrefix(r.Name(), "JSON") }, } documentSchema := reflector.ReflectFromType(reflect.TypeOf(document)) for structName, fields := range comments { if structSchema, exists := documentSchema.Definitions[structName]; exists { if structSchema.Definitions == nil { structSchema.Definitions = make(map[string]*jsonschema.Schema) } for fieldName, comment := range fields { if fieldName == "" { // struct-level comment structSchema.Description = comment continue } // field level comment if comment == "" { continue } if _, exists := structSchema.Properties.Get(fieldName); exists { fieldSchema, exists := structSchema.Definitions[fieldName] if exists { fieldSchema.Description = comment } else { fieldSchema = &jsonschema.Schema{ Description: comment, } } structSchema.Definitions[fieldName] = fieldSchema } } documentSchema.Definitions[structName] = structSchema } } return documentSchema } // parseCommentsFromPackages scans multiple packages and collects field comments for structs. func parseCommentsFromPackages(pkgPatterns []string) map[string]map[string]string { commentMap := make(map[string]map[string]string) cfg := &packages.Config{ Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports, } pkgs, err := packages.Load(cfg, pkgPatterns...) if err != nil { panic(fmt.Errorf("failed to load packages: %w", err)) } for _, pkg := range pkgs { for _, file := range pkg.Syntax { fileComments := parseFileComments(file) for structName, fields := range fileComments { if _, exists := commentMap[structName]; !exists { commentMap[structName] = fields } } } } return commentMap } // parseFileComments extracts comments for structs and their fields in a single file. func parseFileComments(node *ast.File) map[string]map[string]string { commentMap := make(map[string]map[string]string) ast.Inspect(node, func(n ast.Node) bool { ts, ok := n.(*ast.TypeSpec) if !ok { return true } st, ok := ts.Type.(*ast.StructType) if !ok { return true } structName := ts.Name.Name fieldComments := make(map[string]string) // extract struct-level comment if ts.Doc != nil { structComment := strings.TrimSpace(ts.Doc.Text()) if !strings.Contains(structComment, "TODO:") { fieldComments[""] = cleanComment(structComment) } } // extract field-level comments for _, field := range st.Fields.List { if len(field.Names) == 0 { continue } fieldName := field.Names[0].Name jsonTag := getJSONTag(field) if field.Doc != nil { comment := strings.TrimSpace(field.Doc.Text()) if strings.Contains(comment, "TODO:") { continue } if jsonTag != "" { fieldComments[jsonTag] = cleanComment(comment) } else { fieldComments[fieldName] = cleanComment(comment) } } } if len(fieldComments) > 0 { commentMap[structName] = fieldComments } return true }) return commentMap } func cleanComment(comment string) string { // remove the first word, since that is the field name (if following go-doc patterns) split := strings.SplitN(comment, " ", 2) if len(split) > 1 { comment = split[1] } return strings.TrimSpace(strings.ReplaceAll(comment, "\"", "'")) } func getJSONTag(field *ast.Field) string { if field.Tag != nil { tagValue := strings.Trim(field.Tag.Value, "`") structTag := reflect.StructTag(tagValue) if jsonTag, ok := structTag.Lookup("json"); ok { jsonParts := strings.Split(jsonTag, ",") return strings.TrimSpace(jsonParts[0]) } } return "" } func schemaID(component, version string) jsonschema.ID { return jsonschema.ID(fmt.Sprintf("anchore.io/schema/grype/%s/json/%s", component, version)) } func repoRoot() string { root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { panic(fmt.Errorf("unable to find repo root dir: %+v", err)) } absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) if err != nil { panic(fmt.Errorf("unable to get abs path to repo root: %w", err)) } return absRepoRoot } ================================================ FILE: cmd/grype/cli/commands/root.go ================================================ package commands import ( "context" "errors" "fmt" "strings" "time" "github.com/spf13/cobra" "github.com/wagoodman/go-partybus" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/matcher/dotnet" "github.com/anchore/grype/grype/matcher/dpkg" "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/hex" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/rpm" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vex" vexStatus "github.com/anchore/grype/grype/vex/status" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/format" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) func Root(app clio.Application) *cobra.Command { opts := options.DefaultGrype(app.ID()) return app.SetupRootCommand(&cobra.Command{ Use: fmt.Sprintf("%s [IMAGE]", app.ID().Name), Short: "A vulnerability scanner for container images, filesystems, and SBOMs", Long: stringutil.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs. Supports the following image sources: {{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon {{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory You can also explicitly specify the scheme to use: {{.appName}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon {{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon {{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" {{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise) {{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) {{.appName}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk {{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory) {{.appName}} file:path/to/yourfile read directly from a file on disk {{.appName}} sbom:path/to/syft.json read Syft JSON from path on disk {{.appName}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) {{.appName}} purl:path/to/purl/file read a newline separated file of package URLs from a path on disk {{.appName}} PURL read a single package PURL directly (e.g. pkg:apk/openssl@3.2.1?distro=alpine-3.20.3) {{.appName}} cpes:path/to/cpes/file read a newline separated file of package CPEs from a path on disk {{.appName}} CPE read a single CPE directly (e.g. cpe:2.3:a:openssl:openssl:3.0.14:*:*:*:*:*) You can also pipe in Syft JSON directly: syft yourimage:tag -o json | {{.appName}} `, map[string]interface{}{ "appName": app.ID().Name, }), Args: validateRootArgs, SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { userInput := "" if len(args) > 0 { userInput = args[0] } return runGrype(cmd.Context(), app, opts, userInput) }, ValidArgsFunction: dockerImageValidArgsFunction, }, opts) } var ignoreNonFixedMatches = []match.IgnoreRule{ {FixState: string(vulnerability.FixStateNotFixed)}, {FixState: string(vulnerability.FixStateWontFix)}, {FixState: string(vulnerability.FixStateUnknown)}, } var ignoreFixedMatches = []match.IgnoreRule{ {FixState: string(vulnerability.FixStateFixed)}, } var ignoreVEXFixedNotAffected = []match.IgnoreRule{ {VexStatus: string(vexStatus.NotAffected)}, {VexStatus: string(vexStatus.Fixed)}, } var ignoreLinuxKernelHeaders = []match.IgnoreRule{ {Package: match.IgnoreRulePackage{Name: "kernel-headers", UpstreamName: "kernel", Type: string(syftPkg.RpmPkg)}, MatchType: match.ExactIndirectMatch}, {Package: match.IgnoreRulePackage{Name: "linux(-.*)?-headers-.*", UpstreamName: "linux.*", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch}, {Package: match.IgnoreRulePackage{Name: "linux-libc-dev", UpstreamName: "linux", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch}, } //nolint:funlen func runGrype(ctx context.Context, app clio.Application, opts *options.Grype, userInput string) (errs error) { writer, err := format.MakeScanResultWriter(opts.Outputs, opts.File, format.PresentationConfig{ TemplateFilePath: opts.OutputTemplateFile, ShowSuppressed: opts.ShowSuppressed, Pretty: opts.Pretty, }) if err != nil { return err } var vp vulnerability.Provider var status *vulnerability.ProviderStatus var packages []pkg.Package var s *sbom.SBOM var pkgContext pkg.Context if opts.OnlyFixed { opts.Ignore = append(opts.Ignore, ignoreNonFixedMatches...) } if opts.OnlyNotFixed { opts.Ignore = append(opts.Ignore, ignoreFixedMatches...) } if !opts.MatchUpstreamKernelHeaders { opts.Ignore = append(opts.Ignore, ignoreLinuxKernelHeaders...) } for _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) { switch vulnerability.FixState(ignoreState) { case vulnerability.FixStateUnknown, vulnerability.FixStateFixed, vulnerability.FixStateNotFixed, vulnerability.FixStateWontFix: opts.Ignore = append(opts.Ignore, match.IgnoreRule{FixState: ignoreState}) default: return fmt.Errorf("unknown fix state %s was supplied for --ignore-states", ignoreState) } } err = parallel( func() error { checkForAppUpdate(app.ID(), opts) return nil }, func() (err error) { startTime := time.Now() defer func() { validStr := "valid" if err != nil { validStr = "invalid" } log.WithFields("time", time.Since(startTime), "status", validStr).Info("loaded DB") if status != nil { log.WithFields("schema", status.SchemaVersion).Debug("├──") log.WithFields("built", status.Built.UTC().Format(time.RFC3339)).Debug("├──") log.WithFields("from", status.From).Debug("├──") log.WithFields("path", status.Path).Debug("└──") } }() log.Debug("loading DB") vp, status, err = grype.LoadVulnerabilityDB(opts.ToClientConfig(), opts.ToCuratorConfig(), opts.DB.AutoUpdate) return validateDBLoad(err, status) }, func() (err error) { startTime := time.Now() defer func() { log.WithFields("time", time.Since(startTime), "packages", len(packages)).Info("gathered packages") }() log.Debugf("gathering packages") // packages are grype.Package, not syft.Package // the SBOM is returned for downstream formatting concerns // grype uses the SBOM in combination with syft formatters to produce cycloneDX // with vulnerability information appended packages, pkgContext, s, err = pkg.Provide(userInput, getProviderConfig(opts)) if err != nil { return fmt.Errorf("failed to catalog: %w", err) } return nil }, ) if err != nil { return err } defer log.CloseAndLogError(vp, status.Path) warnWhenDistroHintNeeded(packages, &pkgContext) if err = applyVexRules(opts); err != nil { return fmt.Errorf("applying vex rules: %w", err) } startTime := time.Now() vexProcessor, err := vex.NewProcessor(vex.ProcessorOptions{ Documents: opts.VexDocuments, IgnoreRules: opts.Ignore, }) if err != nil { return fmt.Errorf("failed to create VEX processor: %w", err) } vulnMatcher := grype.VulnerabilityMatcher{ VulnerabilityProvider: vp, IgnoreRules: opts.Ignore, NormalizeByCVE: opts.ByCVE, FailSeverity: opts.FailOnSeverity(), Matchers: getMatchers(opts), VexProcessor: vexProcessor, Alerts: grype.AlertsConfig{ EnableEOLDistroWarnings: opts.Alerts.EnableEOLDistroWarnings, }, } remainingMatches, ignoredMatches, err := vulnMatcher.FindMatchesContext(ctx, packages, pkgContext) if err != nil { if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) { return err } errs = appendErrors(errs, err) } log.WithFields("time", time.Since(startTime)).Info("found vulnerability matches") startTime = time.Now() // clear out the registry auth information to avoid including possibly sensitive information in the report opts.Registry.Auth = nil // collect distro alert data from the vulnerability matcher (if enabled) var distroAlertData *models.DistroAlertData if opts.Alerts.EnableEOLDistroWarnings { distroAlertData = &models.DistroAlertData{ EOLDistroPackages: vulnMatcher.EOLDistroPackages(), } warnDistroAlerts(distroAlertData) } model, err := models.NewDocument(app.ID(), packages, pkgContext, *remainingMatches, ignoredMatches, vp, opts, dbInfo(status, vp), models.SortStrategy(opts.SortBy.Criteria), opts.Timestamp, distroAlertData) if err != nil { return fmt.Errorf("failed to create document: %w", err) } if err = writer.Write(models.PresenterConfig{ ID: app.ID(), Document: model, SBOM: s, Pretty: opts.Pretty, }); err != nil { errs = appendErrors(errs, err) } log.WithFields("time", time.Since(startTime)).Trace("wrote vulnerability report") return errs } func warnWhenDistroHintNeeded(pkgs []pkg.Package, context *pkg.Context) { hasOSPackageWithoutDistro := false for _, p := range pkgs { switch p.Type { case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg: if p.Distro == nil { hasOSPackageWithoutDistro = true break } } } if context.Distro == nil && hasOSPackageWithoutDistro { log.Warnf("Unable to determine the OS distribution of some packages. This may result in missing vulnerabilities. " + "You may specify a distro using: --distro :") } } func warnDistroAlerts(data *models.DistroAlertData) { if data == nil { return } // warn about EOL distro packages for distroName, count := range countPackagesByDistro(data.EOLDistroPackages) { msg := fmt.Sprintf("%d packages from EOL distro %q - vulnerability data may be incomplete or outdated; consider upgrading to a supported version", count, distroName) bus.Notify(msg) } } func countPackagesByDistro(packages []pkg.Package) map[string]int { counts := make(map[string]int) for _, p := range packages { distroName := "unknown" if p.Distro != nil { distroName = p.Distro.String() } counts[distroName]++ } return counts } func dbInfo(status *vulnerability.ProviderStatus, vp vulnerability.Provider) any { var providers map[string]vulnerability.DataProvenance if vp != nil { providers = make(map[string]vulnerability.DataProvenance) if dpr, ok := vp.(vulnerability.StoreMetadataProvider); ok { dps, err := dpr.DataProvenance() // ignore errors here if err == nil { providers = dps } } } return struct { Status *vulnerability.ProviderStatus `json:"status"` Providers map[string]vulnerability.DataProvenance `json:"providers"` }{ Status: status, Providers: providers, } } func checkForAppUpdate(id clio.Identification, opts *options.Grype) { if !opts.CheckForAppUpdate { return } isAvailable, newVersion, err := isUpdateAvailable(id) if err != nil { log.Errorf(err.Error()) } if isAvailable { log.Infof("new version of %s is available: %s (currently running: %s)", id.Name, newVersion, id.Version) bus.Publish(partybus.Event{ Type: event.CLIAppUpdateAvailable, Value: parsers.UpdateCheck{ New: newVersion, Current: id.Version, }, }) } else { log.Debugf("no new %s application update available", id.Name) } } func getMatcherConfig(opts *options.Grype) matcher.Config { return matcher.Config{ Java: java.MatcherConfig{ ExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(), UseCPEs: opts.Match.Java.UseCPEs, }, Ruby: ruby.MatcherConfig(opts.Match.Ruby), Python: python.MatcherConfig(opts.Match.Python), Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet), Javascript: javascript.MatcherConfig(opts.Match.Javascript), Golang: golang.MatcherConfig{ UseCPEs: opts.Match.Golang.UseCPEs, AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib, AllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison, }, Hex: hex.MatcherConfig(opts.Match.Hex), Stock: stock.MatcherConfig(opts.Match.Stock), Dpkg: dpkg.MatcherConfig{ MissingEpochStrategy: opts.Match.Dpkg.MissingEpochStrategy, UseCPEsForEOL: opts.Match.Dpkg.UseCPEsForEOL, }, Rpm: rpm.MatcherConfig{ MissingEpochStrategy: opts.Match.Rpm.MissingEpochStrategy, UseCPEsForEOL: opts.Match.Rpm.UseCPEsForEOL, }, } } func getMatchers(opts *options.Grype) []match.Matcher { return matcher.NewDefaultMatchers(getMatcherConfig(opts)) } func getProviderConfig(opts *options.Grype) pkg.ProviderConfig { cfg := syft.DefaultCreateSBOMConfig() cfg.Packages.JavaArchive.IncludeIndexedArchives = opts.Search.IncludeIndexedArchives cfg.Packages.JavaArchive.IncludeUnindexedArchives = opts.Search.IncludeUnindexedArchives // when we run into a package with missing information like version, then this is not useful in the context // of vulnerability matching. Though there will be downstream processing to handle this case, we can still // save us the effort of ever attempting to match with these packages as early as possible. cfg.Compliance.MissingVersion = cataloging.ComplianceActionDrop return pkg.ProviderConfig{ SyftProviderConfig: pkg.SyftProviderConfig{ RegistryOptions: opts.Registry.ToOptions(), Exclusions: opts.Exclusions, SBOMOptions: cfg, Platform: opts.Platform, Name: opts.Name, DefaultImagePullSource: opts.DefaultImagePullSource, Sources: opts.From, }, SynthesisConfig: pkg.SynthesisConfig{ GenerateMissingCPEs: opts.GenerateMissingCPEs, Distro: pkg.DistroConfig{ Override: applyDistroHint(opts.Distro), FixChannels: getFixChannels(opts.FixChannel), }, }, } } func getFixChannels(fixChannelOpts options.FixChannels) distro.FixChannels { // use the API defaults as a starting point, then overlay the application options eusOptions := distro.DefaultFixChannels().Get("eus") if eusOptions == nil { panic("default fix channels do not contain Red Hat EUS channel") } eusOptions.Apply = distro.FixChannelEnabled(fixChannelOpts.RedHatEUS.Apply) if fixChannelOpts.RedHatEUS.Versions != "" { eusOptions.Versions = version.MustGetConstraint(fixChannelOpts.RedHatEUS.Versions, version.SemanticFormat) } return []distro.FixChannel{ { // information inherent to the channel (part of the API defaults) Name: "eus", IDs: eusOptions.IDs, // user configurable options Versions: eusOptions.Versions, Apply: eusOptions.Apply, }, } } func applyDistroHint(hint string) *distro.Distro { if hint == "" { return nil } name, version := distro.ParseDistroString(hint) return distro.NewFromNameVersion(name, version) } func validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error { if loadErr != nil { // notify the user about grype db delete to fix checksum errors if strings.Contains(loadErr.Error(), "checksum") { bus.Notify("Database checksum invalid, run `grype db delete` to remove it and `grype db update` to update.") } if strings.Contains(loadErr.Error(), "import.json") { bus.Notify("Unable to find database import metadata, run `grype db delete` to remove the existing database and `grype db update` to update.") } return fmt.Errorf("failed to load vulnerability db: %w", loadErr) } if status == nil { return fmt.Errorf("unable to determine the status of the vulnerability db") } if status.Error != nil { return fmt.Errorf("db could not be loaded: %w", status.Error) } return nil } func validateRootArgs(cmd *cobra.Command, args []string) error { isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() if err != nil { log.Warnf("unable to determine if there is piped input: %+v", err) isStdinPipeOrRedirect = false } if len(args) == 0 && !isStdinPipeOrRedirect { // in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code. if err := cmd.Help(); err != nil { return fmt.Errorf("unable to display help: %w", err) } return fmt.Errorf("an image/directory argument is required") } // in the case that a single empty string argument ("") is given and there is no piped input we want to show the help text and return with a non-0 return code. if len(args) != 0 && args[0] == "" && !isStdinPipeOrRedirect { if err := cmd.Help(); err != nil { return fmt.Errorf("unable to display help: %w", err) } return fmt.Errorf("an image/directory argument is required") } return cobra.MaximumNArgs(1)(cmd, args) } func applyVexRules(opts *options.Grype) error { // If any vex documents are provided, assume the user intends to ignore vulnerabilities that those // vex documents list as "fixed" or "not_affected". if len(opts.VexDocuments) > 0 { opts.Ignore = append(opts.Ignore, ignoreVEXFixedNotAffected...) } for _, status := range opts.VexAdd { switch status { case string(vexStatus.Affected): opts.Ignore = append( opts.Ignore, match.IgnoreRule{VexStatus: string(vexStatus.Affected)}, ) case string(vexStatus.UnderInvestigation): opts.Ignore = append( opts.Ignore, match.IgnoreRule{VexStatus: string(vexStatus.UnderInvestigation)}, ) default: return fmt.Errorf("invalid VEX status in vex-add setting: %s", status) } } return nil } ================================================ FILE: cmd/grype/cli/commands/root_test.go ================================================ package commands import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/matcher/dotnet" "github.com/anchore/grype/grype/matcher/dpkg" "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/hex" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/rpm" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" vexStatus "github.com/anchore/grype/grype/vex/status" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/pkg/cataloger/binary" ) func Test_getProviderConfig(t *testing.T) { tests := []struct { name string opts *options.Grype want pkg.ProviderConfig }{ { name: "syft default api options are used", opts: options.DefaultGrype(clio.Identification{ Name: "test", Version: "1.0", }), want: pkg.ProviderConfig{ SyftProviderConfig: pkg.SyftProviderConfig{ SBOMOptions: func() *syft.CreateSBOMConfig { cfg := syft.DefaultCreateSBOMConfig() cfg.Compliance.MissingVersion = cataloging.ComplianceActionDrop return cfg }(), RegistryOptions: &image.RegistryOptions{ Credentials: []image.RegistryCredentials{}, }, }, SynthesisConfig: pkg.SynthesisConfig{ GenerateMissingCPEs: false, Distro: pkg.DistroConfig{ Override: nil, FixChannels: []distro.FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: "auto", Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat), }, }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opts := cmp.Options{ cmpopts.IgnoreFields(binary.Classifier{}, "EvidenceMatcher"), cmpopts.IgnoreUnexported(syft.CreateSBOMConfig{}), } if d := cmp.Diff(tt.want, getProviderConfig(tt.opts), opts...); d != "" { t.Errorf("getProviderConfig() mismatch (-want +got):\n%s", d) } }) } } func Test_getMatcherConfig(t *testing.T) { tests := []struct { name string opts *options.Grype want matcher.Config }{ { name: "default options", opts: options.DefaultGrype(clio.Identification{ Name: "test", Version: "1.0", }), want: matcher.Config{ Java: java.MatcherConfig{ ExternalSearchConfig: java.ExternalSearchConfig{ SearchMavenUpstream: false, MavenBaseURL: "https://search.maven.org/solrsearch/select", MavenRateLimit: 300000000, // 300ms in nanoseconds }, UseCPEs: false, }, Ruby: ruby.MatcherConfig{}, Python: python.MatcherConfig{}, Dotnet: dotnet.MatcherConfig{}, Javascript: javascript.MatcherConfig{}, Golang: golang.MatcherConfig{ UseCPEs: false, AlwaysUseCPEForStdlib: true, AllowMainModulePseudoVersionComparison: false, }, Hex: hex.MatcherConfig{}, Stock: stock.MatcherConfig{UseCPEs: true}, Rpm: rpm.MatcherConfig{ MissingEpochStrategy: "auto", }, Dpkg: dpkg.MatcherConfig{ MissingEpochStrategy: "zero", }, }, }, { name: "rpm missing-epoch-strategy set to zero", opts: func() *options.Grype { opts := options.DefaultGrype(clio.Identification{Name: "test", Version: "1.0"}) opts.Match.Rpm.MissingEpochStrategy = "zero" return opts }(), want: matcher.Config{ Java: java.MatcherConfig{ ExternalSearchConfig: java.ExternalSearchConfig{ SearchMavenUpstream: false, MavenBaseURL: "https://search.maven.org/solrsearch/select", MavenRateLimit: 300000000, }, UseCPEs: false, }, Ruby: ruby.MatcherConfig{}, Python: python.MatcherConfig{}, Dotnet: dotnet.MatcherConfig{}, Javascript: javascript.MatcherConfig{}, Golang: golang.MatcherConfig{ UseCPEs: false, AlwaysUseCPEForStdlib: true, AllowMainModulePseudoVersionComparison: false, }, Hex: hex.MatcherConfig{}, Stock: stock.MatcherConfig{UseCPEs: true}, Rpm: rpm.MatcherConfig{ MissingEpochStrategy: "zero", }, Dpkg: dpkg.MatcherConfig{ MissingEpochStrategy: "zero", }, }, }, { name: "dpkg missing-epoch-strategy set to auto", opts: func() *options.Grype { opts := options.DefaultGrype(clio.Identification{Name: "test", Version: "1.0"}) opts.Match.Dpkg.MissingEpochStrategy = "auto" return opts }(), want: matcher.Config{ Java: java.MatcherConfig{ ExternalSearchConfig: java.ExternalSearchConfig{ SearchMavenUpstream: false, MavenBaseURL: "https://search.maven.org/solrsearch/select", MavenRateLimit: 300000000, }, UseCPEs: false, }, Ruby: ruby.MatcherConfig{}, Python: python.MatcherConfig{}, Dotnet: dotnet.MatcherConfig{}, Javascript: javascript.MatcherConfig{}, Golang: golang.MatcherConfig{ UseCPEs: false, AlwaysUseCPEForStdlib: true, AllowMainModulePseudoVersionComparison: false, }, Hex: hex.MatcherConfig{}, Stock: stock.MatcherConfig{UseCPEs: true}, Rpm: rpm.MatcherConfig{ MissingEpochStrategy: "auto", }, Dpkg: dpkg.MatcherConfig{ MissingEpochStrategy: "auto", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if d := cmp.Diff(tt.want, getMatcherConfig(tt.opts)); d != "" { t.Errorf("getMatcherConfig() mismatch (-want +got):\n%s", d) } }) } } func Test_applyVexRules(t *testing.T) { tests := []struct { name string initialIgnoreRules []match.IgnoreRule vexDocuments []string vexAdd []string expectedIgnoreRules []match.IgnoreRule expectError bool expectedErrorSubstring string }{ { name: "no VEX documents provided - no rules added", initialIgnoreRules: []match.IgnoreRule{}, vexDocuments: []string{}, vexAdd: []string{}, expectedIgnoreRules: []match.IgnoreRule{}, expectError: false, }, { name: "VEX documents provided with empty ignore rules - automatic rules added", initialIgnoreRules: []match.IgnoreRule{}, vexDocuments: []string{"path/to/vex.json"}, vexAdd: []string{}, expectedIgnoreRules: []match.IgnoreRule{ {VexStatus: string(vexStatus.NotAffected)}, {VexStatus: string(vexStatus.Fixed)}, }, expectError: false, }, { name: "VEX documents provided with existing ignore rules - automatic rules still added", initialIgnoreRules: []match.IgnoreRule{ {Vulnerability: "CVE-2023-1234"}, }, vexDocuments: []string{"path/to/vex.json"}, vexAdd: []string{}, expectedIgnoreRules: []match.IgnoreRule{ {Vulnerability: "CVE-2023-1234"}, {VexStatus: string(vexStatus.NotAffected)}, {VexStatus: string(vexStatus.Fixed)}, }, expectError: false, }, { name: "vex-add with valid statuses", initialIgnoreRules: []match.IgnoreRule{}, vexDocuments: []string{"path/to/vex.json"}, vexAdd: []string{"affected", "under_investigation"}, expectedIgnoreRules: []match.IgnoreRule{ {VexStatus: string(vexStatus.NotAffected)}, {VexStatus: string(vexStatus.Fixed)}, {VexStatus: string(vexStatus.Affected)}, {VexStatus: string(vexStatus.UnderInvestigation)}, }, expectError: false, }, { name: "vex-add with invalid status", initialIgnoreRules: []match.IgnoreRule{}, vexDocuments: []string{"path/to/vex.json"}, vexAdd: []string{"invalid_status"}, expectedIgnoreRules: nil, expectError: true, expectedErrorSubstring: "invalid VEX status in vex-add setting: invalid_status", }, { name: "vex-add attempting to use fixed status", initialIgnoreRules: []match.IgnoreRule{}, vexDocuments: []string{"path/to/vex.json"}, vexAdd: []string{"fixed"}, expectedIgnoreRules: nil, expectError: true, expectedErrorSubstring: "invalid VEX status in vex-add setting: fixed", }, { name: "multiple VEX documents with existing rules", initialIgnoreRules: []match.IgnoreRule{ {Vulnerability: "CVE-2023-1234"}, {FixState: "unknown"}, }, vexDocuments: []string{"vex1.json", "vex2.json"}, vexAdd: []string{"affected"}, expectedIgnoreRules: []match.IgnoreRule{ {Vulnerability: "CVE-2023-1234"}, {FixState: "unknown"}, {VexStatus: string(vexStatus.NotAffected)}, {VexStatus: string(vexStatus.Fixed)}, {VexStatus: string(vexStatus.Affected)}, }, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opts := &options.Grype{ Ignore: append([]match.IgnoreRule{}, tt.initialIgnoreRules...), VexDocuments: tt.vexDocuments, VexAdd: tt.vexAdd, } err := applyVexRules(opts) if tt.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedErrorSubstring) return } require.NoError(t, err) assert.Equal(t, tt.expectedIgnoreRules, opts.Ignore) }) } } ================================================ FILE: cmd/grype/cli/commands/testdata/provider-metadata.json ================================================ { "providers": [ { "name": "provider1", "lastSuccessfulRun": "2024-10-16T01:33:16.844201Z" }, { "name": "provider2", "lastSuccessfulRun": "2024-10-16T01:32:43.516596Z" } ] } ================================================ FILE: cmd/grype/cli/commands/update.go ================================================ package commands import ( "fmt" "io" "net/http" "strings" "github.com/anchore/clio" hashiVersion "github.com/anchore/go-version" "github.com/anchore/grype/cmd/grype/internal" ) var latestAppVersionURL = struct { host string path string }{ host: "https://toolbox-data.anchore.io", path: "/grype/releases/latest/VERSION", } func isProductionBuild(version string) bool { if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) { return false } return true } func isUpdateAvailable(id clio.Identification) (bool, string, error) { if !isProductionBuild(id.Version) { // don't allow for non-production builds to check for a version. return false, "", nil } currentVersion, err := hashiVersion.NewVersion(id.Version) if err != nil { return false, "", fmt.Errorf("failed to parse current application version: %w", err) } latestVersion, err := fetchLatestApplicationVersion(id) if err != nil { return false, "", err } if latestVersion.GreaterThan(currentVersion) { return true, latestVersion.String(), nil } return false, "", nil } func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) { req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil) if err != nil { return nil, fmt.Errorf("failed to create request for latest version: %w", err) } req.Header.Add("User-Agent", fmt.Sprintf("%v %v", id.Name, id.Version)) client := http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch latest version: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status) } versionBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read latest version: %w", err) } versionStr := strings.TrimSuffix(string(versionBytes), "\n") if len(versionStr) > 50 { return nil, fmt.Errorf("version too long: %q", versionStr[:50]) } return hashiVersion.NewVersion(versionStr) } ================================================ FILE: cmd/grype/cli/commands/update_test.go ================================================ package commands import ( "net/http" "net/http/httptest" "testing" "github.com/anchore/clio" hashiVersion "github.com/anchore/go-version" "github.com/anchore/grype/cmd/grype/internal" ) func TestIsUpdateAvailable(t *testing.T) { tests := []struct { name string buildVersion string latestVersion string code int isAvailable bool newVersion string err bool }{ { name: "equal", buildVersion: "1.0.0", latestVersion: "1.0.0", code: 200, isAvailable: false, newVersion: "", err: false, }, { name: "hasUpdate", buildVersion: "1.0.0", latestVersion: "1.2.0", code: 200, isAvailable: true, newVersion: "1.2.0", err: false, }, { name: "aheadOfLatest", buildVersion: "1.2.0", latestVersion: "1.0.0", code: 200, isAvailable: false, newVersion: "", err: false, }, { name: "EmptyUpdate", buildVersion: "1.0.0", latestVersion: "", code: 200, isAvailable: false, newVersion: "", err: true, }, { name: "GarbageUpdate", buildVersion: "1.0.0", latestVersion: "hdfjksdhfhkj", code: 200, isAvailable: false, newVersion: "", err: true, }, { name: "BadUpdate", buildVersion: "1.0.0", latestVersion: "1.0.", code: 500, isAvailable: false, newVersion: "", err: true, }, { name: "NoBuildVersion", buildVersion: internal.NotProvided, latestVersion: "1.0.0", code: 200, isAvailable: false, newVersion: "", err: false, }, { name: "BadUpdateValidVersion", buildVersion: "1.0.0", latestVersion: "2.0.0", code: 404, isAvailable: false, newVersion: "", err: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // setup mocks // local... version := test.buildVersion // remote... handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.code) _, _ = w.Write([]byte(test.latestVersion)) }) mockSrv := httptest.NewServer(handler) latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() isAvailable, newVersion, err := isUpdateAvailable(clio.Identification{Version: version}) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { t.Fatalf("expected error but got none") } if newVersion != test.newVersion { t.Errorf("unexpected NEW version: %+v", newVersion) } if isAvailable != test.isAvailable { t.Errorf("unexpected result: %+v", isAvailable) } }) } } func TestFetchLatestApplicationVersion(t *testing.T) { tests := []struct { name string response string code int err bool expected *hashiVersion.Version }{ { name: "gocase", response: "1.0.0", code: 200, expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")), }, { name: "garbage", response: "garbage", code: 200, expected: nil, err: true, }, { name: "http 500", response: "1.0.0", code: 500, expected: nil, err: true, }, { name: "http 404", response: "1.0.0", code: 404, expected: nil, err: true, }, { name: "empty", response: "", code: 200, expected: nil, err: true, }, { name: "too long", response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ", code: 200, expected: nil, err: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // setup mock handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.code) _, _ = w.Write([]byte(test.response)) }) mockSrv := httptest.NewServer(handler) latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() actual, err := fetchLatestApplicationVersion(clio.Identification{}) if err != nil && !test.err { t.Fatalf("got error but expected none: %+v", err) } else if err == nil && test.err { t.Fatalf("expected error but got none") } if err != nil { return } if actual.String() != test.expected.String() { t.Errorf("unexpected version: %+v", actual.String()) } }) } } func Test_UserAgent(t *testing.T) { got := "" // setup mock handler := http.NewServeMux() handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { got = r.Header.Get("User-Agent") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("1.0.0")) }) mockSrv := httptest.NewServer(handler) latestAppVersionURL.host = mockSrv.URL defer mockSrv.Close() fetchLatestApplicationVersion(clio.Identification{ Name: "the-app", Version: "v3.2.1", }) if got != "the-app v3.2.1" { t.Errorf("expected User-Agent header to match, got: %v", got) } } ================================================ FILE: cmd/grype/cli/commands/util.go ================================================ package commands import ( "fmt" "io" "os" "strings" "sync" "github.com/hashicorp/go-multierror" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "github.com/spf13/cobra" "golang.org/x/exp/maps" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/internal/ui" ) func disableUI(app clio.Application) func(*cobra.Command, []string) error { return func(_ *cobra.Command, _ []string) error { type Stater interface { State() *clio.State } state := app.(Stater).State() state.UI = clio.NewUICollection(ui.None(state.Config.Log.Quiet)) return nil } } func stderrPrintLnf(message string, args ...interface{}) error { if !strings.HasSuffix(message, "\n") { message += "\n" } _, err := fmt.Fprintf(os.Stderr, message, args...) return err } // parallel takes a set of functions and runs them in parallel, capturing all errors returned and // returning the single error returned by one of the parallel funcs, or a multierror.Error with all // the errors if more than one func parallel(funcs ...func() error) error { errs := parallelMapped(funcs...) if len(errs) > 0 { values := maps.Values(errs) if len(values) == 1 { return values[0] } return multierror.Append(nil, values...) } return nil } // parallelMapped takes a set of functions and runs them in parallel, capturing all errors returned in // a map indicating which func, by index returned which error func parallelMapped(funcs ...func() error) map[int]error { errs := map[int]error{} errorLock := &sync.Mutex{} wg := &sync.WaitGroup{} wg.Add(len(funcs)) for i, fn := range funcs { go func(i int, fn func() error) { defer wg.Done() err := fn() if err != nil { errorLock.Lock() defer errorLock.Unlock() errs[i] = err } }(i, fn) } wg.Wait() return errs } func appendErrors(errs error, err ...error) error { if errs == nil { switch len(err) { case 0: return nil case 1: return err[0] } } return multierror.Append(errs, err...) } func newTable(output io.Writer, columns []string) *tablewriter.Table { return tablewriter.NewTable(output, tablewriter.WithHeader(columns), tablewriter.WithHeaderAlignment(tw.AlignLeft), tablewriter.WithHeaderAutoWrap(tw.WrapNone), tablewriter.WithRowAutoWrap(tw.WrapNone), tablewriter.WithAutoHide(tw.On), tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithBehavior( tw.Behavior{ TrimSpace: tw.On, AutoHide: tw.On, }, ), tablewriter.WithPadding( tw.Padding{ Right: " ", }, ), tablewriter.WithRendition( tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleNone), Settings: tw.Settings{ Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, }, }, ), ) } ================================================ FILE: cmd/grype/cli/commands/util_test.go ================================================ package commands import ( "fmt" "sync" "sync/atomic" "testing" "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/require" ) const lotsaParallel = 100 func Test_lotsaLotsaParallel(t *testing.T) { funcs := []func() error{} for i := 0; i < lotsaParallel; i++ { funcs = append(funcs, func() error { Test_lotsaParallel(t) return nil }) } err := parallel(funcs...) require.NoError(t, err) } func Test_lotsaParallel(t *testing.T) { for i := 0; i < lotsaParallel; i++ { Test_parallel(t) } } // Test_parallel tests the parallel function by executing a set of functions that can only execute in a specific // order if they are actually running in parallel. func Test_parallel(t *testing.T) { count := atomic.Int32{} count.Store(0) wg1 := sync.WaitGroup{} wg1.Add(1) wg2 := sync.WaitGroup{} wg2.Add(1) wg3 := sync.WaitGroup{} wg3.Add(1) err1 := fmt.Errorf("error-1") err2 := fmt.Errorf("error-2") err3 := fmt.Errorf("error-3") order := "" got := parallel( func() error { wg1.Wait() count.Add(1) order = order + "_0" return nil }, func() error { wg3.Wait() defer wg2.Done() count.Add(10) order = order + "_1" return err1 }, func() error { wg2.Wait() defer wg1.Done() count.Add(100) order = order + "_2" return err2 }, func() error { defer wg3.Done() count.Add(1000) order = order + "_3" return err3 }, ) require.Equal(t, int32(1111), count.Load()) require.Equal(t, "_3_1_2_0", order) errs := got.(*multierror.Error).Errors // cannot check equality to a slice with err1,2,3 because the functions above are running in parallel, for example: // after func()#4 returns and the `wg3.Done()` has executed, the thread could immediately pause // and the remaining functions execute first and err3 becomes the last in the list instead of the first require.Contains(t, errs, err1) require.Contains(t, errs, err2) require.Contains(t, errs, err3) } func Test_parallelMapped(t *testing.T) { err0 := fmt.Errorf("error-0") err1 := fmt.Errorf("error-1") err2 := fmt.Errorf("error-2") tests := []struct { name string funcs []func() error expected map[int]error }{ { name: "basic", funcs: []func() error{ func() error { return nil }, func() error { return err1 }, func() error { return nil }, func() error { return err2 }, }, expected: map[int]error{ 1: err1, 3: err2, }, }, { name: "no errors", funcs: []func() error{ func() error { return nil }, func() error { return nil }, }, expected: map[int]error{}, }, { name: "all errors", funcs: []func() error{ func() error { return err0 }, func() error { return err1 }, func() error { return err2 }, }, expected: map[int]error{ 0: err0, 1: err1, 2: err2, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := parallelMapped(test.funcs...) require.Equal(t, test.expected, got) }) } } ================================================ FILE: cmd/grype/cli/options/alerts.go ================================================ package options import "github.com/anchore/clio" // Alerts configures how alerts are generated and displayed. type Alerts struct { // EnableEOLDistroWarnings enables warnings about packages from end-of-life distros EnableEOLDistroWarnings bool `yaml:"enable-eol-distro-warnings" json:"enable-eol-distro-warnings" mapstructure:"enable-eol-distro-warnings"` } var _ clio.FieldDescriber = (*Alerts)(nil) func defaultAlerts() Alerts { return Alerts{ EnableEOLDistroWarnings: true, } } func (a *Alerts) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&a.EnableEOLDistroWarnings, `enable/disable warnings about packages from end-of-life (EOL) distros. When enabled, grype will track and report packages that come from distros that have reached their end-of-life date.`) } ================================================ FILE: cmd/grype/cli/options/alerts_test.go ================================================ package options import ( "testing" "github.com/stretchr/testify/assert" ) func TestDefaultAlerts(t *testing.T) { alerts := defaultAlerts() // EOL distro warnings should be enabled by default assert.True(t, alerts.EnableEOLDistroWarnings, "EnableEOLDistroWarnings should be true by default") } ================================================ FILE: cmd/grype/cli/options/database.go ================================================ package options import ( "time" "github.com/anchore/clio" "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" ) type Database struct { ID clio.Identification `yaml:"-" json:"-" mapstructure:"-"` Dir string `yaml:"cache-dir" json:"cache-dir" mapstructure:"cache-dir"` UpdateURL string `yaml:"update-url" json:"update-url" mapstructure:"update-url"` CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` AutoUpdate bool `yaml:"auto-update" json:"auto-update" mapstructure:"auto-update"` ValidateByHashOnStart bool `yaml:"validate-by-hash-on-start" json:"validate-by-hash-on-start" mapstructure:"validate-by-hash-on-start"` ValidateAge bool `yaml:"validate-age" json:"validate-age" mapstructure:"validate-age"` MaxAllowedBuiltAge time.Duration `yaml:"max-allowed-built-age" json:"max-allowed-built-age" mapstructure:"max-allowed-built-age"` RequireUpdateCheck bool `yaml:"require-update-check" json:"require-update-check" mapstructure:"require-update-check"` UpdateAvailableTimeout time.Duration `yaml:"update-available-timeout" json:"update-available-timeout" mapstructure:"update-available-timeout"` UpdateDownloadTimeout time.Duration `yaml:"update-download-timeout" json:"update-download-timeout" mapstructure:"update-download-timeout"` MaxUpdateCheckFrequency time.Duration `yaml:"max-update-check-frequency" json:"max-update-check-frequency" mapstructure:"max-update-check-frequency"` } var _ interface { clio.FieldDescriber clio.PostLoader } = (*Database)(nil) func DefaultDatabase(id clio.Identification) Database { distConfig := distribution.DefaultConfig() installConfig := installation.DefaultConfig(id) return Database{ ID: id, Dir: installConfig.DBRootDir, UpdateURL: distConfig.LatestURL, AutoUpdate: true, ValidateAge: installConfig.ValidateAge, // After this period (5 days) the db data is considered stale MaxAllowedBuiltAge: installConfig.MaxAllowedBuiltAge, RequireUpdateCheck: distConfig.RequireUpdateCheck, ValidateByHashOnStart: installConfig.ValidateChecksum, UpdateAvailableTimeout: distConfig.CheckTimeout, UpdateDownloadTimeout: distConfig.UpdateTimeout, MaxUpdateCheckFrequency: installConfig.UpdateCheckMaxFrequency, CACert: distConfig.CACert, } } func (cfg *Database) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&cfg.Dir, `location to write the vulnerability database cache`) descriptions.Add(&cfg.UpdateURL, `URL of the vulnerability database`) descriptions.Add(&cfg.CACert, `certificate to trust download the database and listing file`) descriptions.Add(&cfg.AutoUpdate, `check for database updates on execution`) descriptions.Add(&cfg.ValidateAge, `ensure db build is no older than the max-allowed-built-age`) descriptions.Add(&cfg.ValidateByHashOnStart, `validate the database matches the known hash each execution`) descriptions.Add(&cfg.MaxAllowedBuiltAge, `Max allowed age for vulnerability database, age being the time since it was built Default max age is 120h (or five days)`) descriptions.Add(&cfg.RequireUpdateCheck, `fail the scan if unable to check for database updates`) descriptions.Add(&cfg.UpdateAvailableTimeout, `Timeout for downloading GRYPE_DB_UPDATE_URL to see if the database needs to be downloaded This file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as needed`) descriptions.Add(&cfg.UpdateDownloadTimeout, `Timeout for downloading actual vulnerability DB The DB is ~156MB as of 2024-04-17 so slower connections may exceed the default timeout; adjust as needed`) descriptions.Add(&cfg.MaxUpdateCheckFrequency, `Maximum frequency to check for vulnerability database updates`) } func (cfg *Database) PostLoad() error { var err error cfg.Dir, err = homedir.Expand(cfg.Dir) return err } ================================================ FILE: cmd/grype/cli/options/database_command.go ================================================ package options import ( "github.com/anchore/clio" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" ) type DatabaseCommand struct { DB Database `yaml:"db" json:"db" mapstructure:"db"` Experimental Experimental `yaml:"exp" json:"exp" mapstructure:"exp"` Developer developer `yaml:"dev" json:"dev" mapstructure:"dev"` } func DefaultDatabaseCommand(id clio.Identification) *DatabaseCommand { dbDefaults := DefaultDatabase(id) // by default, require update check success for db operations which check for updates dbDefaults.RequireUpdateCheck = true // we want to validate by hash during Status checks dbDefaults.ValidateByHashOnStart = true return &DatabaseCommand{ DB: dbDefaults, } } func (cfg DatabaseCommand) ToCuratorConfig() installation.Config { return installation.Config{ DBRootDir: cfg.DB.Dir, ValidateAge: cfg.DB.ValidateAge, ValidateChecksum: cfg.DB.ValidateByHashOnStart, MaxAllowedBuiltAge: cfg.DB.MaxAllowedBuiltAge, UpdateCheckMaxFrequency: cfg.DB.MaxUpdateCheckFrequency, Debug: cfg.Developer.DB.Debug, } } func (cfg DatabaseCommand) ToClientConfig() distribution.Config { return distribution.Config{ ID: cfg.DB.ID, LatestURL: cfg.DB.UpdateURL, CACert: cfg.DB.CACert, RequireUpdateCheck: cfg.DB.RequireUpdateCheck, CheckTimeout: cfg.DB.UpdateAvailableTimeout, UpdateTimeout: cfg.DB.UpdateDownloadTimeout, } } ================================================ FILE: cmd/grype/cli/options/database_search_bounds.go ================================================ package options import ( "fmt" "github.com/anchore/clio" ) type DBSearchBounds struct { RecordLimit int `yaml:"limit" json:"limit" mapstructure:"limit"` } func DefaultDBSearchBounds() DBSearchBounds { return DBSearchBounds{ RecordLimit: 5000, } } func (o *DBSearchBounds) AddFlags(flags clio.FlagSet) { flags.IntVarP(&o.RecordLimit, "limit", "", "limit the number of results returned, use 0 for no limit") } func (o *DBSearchBounds) PostLoad() error { if o.RecordLimit < 0 { return fmt.Errorf("limit must be a positive integer") } return nil } ================================================ FILE: cmd/grype/cli/options/database_search_format.go ================================================ package options import ( "fmt" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/clio" ) type DBSearchFormat struct { Output string `yaml:"output" json:"output" mapstructure:"output"` Allowable []string `yaml:"-" json:"-" mapstructure:"-"` } func DefaultDBSearchFormat() DBSearchFormat { return DBSearchFormat{ Output: "table", Allowable: []string{"table", "json"}, } } func (c *DBSearchFormat) AddFlags(flags clio.FlagSet) { available := strings.Join(c.Allowable, ", ") flags.StringVarP(&c.Output, "output", "o", fmt.Sprintf("format to display results (available=[%s])", available)) } func (c *DBSearchFormat) PostLoad() error { if len(c.Allowable) > 0 { if !strset.New(c.Allowable...).Has(c.Output) { return fmt.Errorf("invalid output format: %s (expected one of: %s)", c.Output, strings.Join(c.Allowable, ", ")) } } return nil } ================================================ FILE: cmd/grype/cli/options/database_search_os.go ================================================ package options import ( "fmt" "strings" "unicode" "github.com/anchore/clio" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/distro" ) type DBSearchOSs struct { OSs []string `yaml:"distro" json:"distro" mapstructure:"distro"` Specs v6.OSSpecifiers `yaml:"-" json:"-" mapstructure:"-"` } func (o *DBSearchOSs) AddFlags(flags clio.FlagSet) { // consistent with grype --distro flag today flags.StringArrayVarP(&o.OSs, "distro", "", "refine to results with the given operating system (format: 'name', 'name[-:@]version', 'name[-:@]maj.min', 'name[-:@]codename')") } func (o *DBSearchOSs) PostLoad() error { if len(o.OSs) == 0 { o.Specs = []*v6.OSSpecifier{v6.AnyOSSpecified} return nil } var specs []*v6.OSSpecifier for _, osValue := range o.OSs { spec, err := parseOSString(osValue) if err != nil { return err } specs = append(specs, spec) } o.Specs = specs return nil } func parseOSString(osValue string) (*v6.OSSpecifier, error) { // Check for multiple @ separators in the original input (not allowed) if strings.Count(osValue, "@") > 1 { return nil, fmt.Errorf("invalid distro name@version: %q", osValue) } // Use the shared parsing logic from grype/distro package name, version := distro.ParseDistroString(osValue) if name == "" { return nil, fmt.Errorf("invalid distro input provided: %q", osValue) } // Check if there was a separator but no version (e.g., "ubuntu@") // This can be detected by checking if the original string ends with a separator originalTrimmed := strings.TrimSpace(osValue) if len(originalTrimmed) > 0 && version == "" { lastChar := originalTrimmed[len(originalTrimmed)-1] if lastChar == '-' || lastChar == ':' || lastChar == '@' { return nil, fmt.Errorf("invalid distro version provided") } } // No version specified if version == "" { return &v6.OSSpecifier{Name: name}, nil } // parse the version (major.minor, major, or codename) // if starts with a number, then it is a version if unicode.IsDigit(rune(version[0])) { versionParts := strings.Split(version, ".") var major, minor string switch len(versionParts) { case 1: major = versionParts[0] case 2: major = versionParts[0] minor = versionParts[1] case 3: return nil, fmt.Errorf("invalid distro version provided: patch version ignored: %q", version) default: return nil, fmt.Errorf("invalid distro version provided: %q", version) } return &v6.OSSpecifier{Name: name, MajorVersion: major, MinorVersion: minor}, nil } // is codename / label return &v6.OSSpecifier{Name: name, LabelVersion: version}, nil } ================================================ FILE: cmd/grype/cli/options/database_search_os_test.go ================================================ package options import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" v6 "github.com/anchore/grype/grype/db/v6" ) func TestDBSearchOSsPostLoad(t *testing.T) { testCases := []struct { name string input DBSearchOSs expectedSpecs v6.OSSpecifiers expectedErrMsg string }{ { name: "no OS input (any OS)", input: DBSearchOSs{}, expectedSpecs: []*v6.OSSpecifier{v6.AnyOSSpecified}, }, { name: "valid OS name only", input: DBSearchOSs{ OSs: []string{"ubuntu"}, }, expectedSpecs: []*v6.OSSpecifier{ {Name: "ubuntu"}, }, }, { name: "valid OS with major version", input: DBSearchOSs{ OSs: []string{"ubuntu@20"}, }, expectedSpecs: []*v6.OSSpecifier{ {Name: "ubuntu", MajorVersion: "20"}, }, }, { name: "valid OS with major and minor version", input: DBSearchOSs{ OSs: []string{"ubuntu@20.04"}, }, expectedSpecs: []*v6.OSSpecifier{ {Name: "ubuntu", MajorVersion: "20", MinorVersion: "04"}, }, }, { name: "valid OS with codename", input: DBSearchOSs{ OSs: []string{"ubuntu@focal"}, }, expectedSpecs: []*v6.OSSpecifier{ {Name: "ubuntu", LabelVersion: "focal"}, }, }, { name: "invalid OS version (too many parts)", input: DBSearchOSs{ OSs: []string{"ubuntu@20.04.1"}, }, expectedErrMsg: "invalid distro version provided: patch version ignored", }, { name: "invalid OS format with colon", input: DBSearchOSs{ OSs: []string{"ubuntu:20"}, }, expectedSpecs: []*v6.OSSpecifier{ {Name: "ubuntu", MajorVersion: "20"}, }, }, { name: "invalid OS with empty version", input: DBSearchOSs{ OSs: []string{"ubuntu@"}, }, expectedErrMsg: "invalid distro version provided", }, { name: "invalid OS name@version format", input: DBSearchOSs{ OSs: []string{"ubuntu@20@04"}, }, expectedErrMsg: "invalid distro name@version", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.input.PostLoad() if tc.expectedErrMsg != "" { require.Error(t, err) require.ErrorContains(t, err, tc.expectedErrMsg) return } require.NoError(t, err) if d := cmp.Diff(tc.expectedSpecs, tc.input.Specs); d != "" { t.Errorf("unexpected OS specifiers (-want +got):\n%s", d) } }) } } ================================================ FILE: cmd/grype/cli/options/database_search_packages.go ================================================ package options import ( "errors" "fmt" "strings" "github.com/anchore/clio" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/log" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/cpe" ) type DBSearchPackages struct { AllowBroadCPEMatching bool `yaml:"allow-broad-cpe-matching" json:"allow-broad-cpe-matching" mapstructure:"allow-broad-cpe-matching"` Packages []string `yaml:"packages" json:"packages" mapstructure:"packages"` Ecosystem string `yaml:"ecosystem" json:"ecosystem" mapstructure:"ecosystem"` PkgSpecs v6.PackageSpecifiers `yaml:"-" json:"-" mapstructure:"-"` CPESpecs v6.PackageSpecifiers `yaml:"-" json:"-" mapstructure:"-"` } func (o *DBSearchPackages) AddFlags(flags clio.FlagSet) { flags.StringArrayVarP(&o.Packages, "pkg", "", "package name/CPE/PURL to search for") flags.StringVarP(&o.Ecosystem, "ecosystem", "", "ecosystem of the package to search within") flags.BoolVarP(&o.AllowBroadCPEMatching, "broad-cpe-matching", "", "allow for specific package CPE attributes to match with '*' values on the vulnerability") } func (o *DBSearchPackages) PostLoad() error { // note: this may be called multiple times, so we need to reset the specs each time o.PkgSpecs = nil o.CPESpecs = nil for _, p := range o.Packages { switch { case strings.HasPrefix(p, "cpe:"): c, err := cpe.NewAttributes(p) if err != nil { return fmt.Errorf("invalid CPE from %q: %w", o.Packages, err) } if c.Version != "" || c.Update != "" { log.Warnf("ignoring version and update values for %q", p) c.Version = "" c.Update = "" } s := &v6.PackageSpecifier{CPE: &c} o.CPESpecs = append(o.CPESpecs, s) o.PkgSpecs = append(o.PkgSpecs, s) case strings.HasPrefix(p, "pkg:"): if o.Ecosystem != "" { return errors.New("cannot specify both package URL and ecosystem") } purl, err := packageurl.FromString(p) if err != nil { return fmt.Errorf("invalid package URL from %q: %w", o.Packages, err) } if purl.Version != "" || len(purl.Qualifiers) > 0 { log.Warnf("ignoring version and qualifiers for package URL %q", purl) } o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: purl.Name, Ecosystem: purl.Type}) o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: "a", Product: purl.Name, TargetSW: purl.Type}}) default: o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: p, Ecosystem: o.Ecosystem}) o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{ CPE: &cpe.Attributes{Part: "a", Product: p}, }) } } if len(o.Packages) == 0 { if o.Ecosystem != "" { o.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Ecosystem: o.Ecosystem}) o.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{TargetSW: o.Ecosystem}}) } } return nil } ================================================ FILE: cmd/grype/cli/options/database_search_packages_test.go ================================================ package options import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/syft/syft/cpe" ) func TestDBSearchPackagesPostLoad(t *testing.T) { testCases := []struct { name string input DBSearchPackages expectedPkg v6.PackageSpecifiers expectedCPE v6.PackageSpecifiers expectedErrMsg string }{ { name: "valid CPE", input: DBSearchPackages{ Packages: []string{"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*"}, }, expectedPkg: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor", Product: "product"}}, }, expectedCPE: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Vendor: "vendor", Product: "product"}}, }, }, { name: "valid PURL", input: DBSearchPackages{ Packages: []string{"pkg:npm/package-name@1.0.0"}, }, expectedPkg: v6.PackageSpecifiers{ {Name: "package-name", Ecosystem: "npm"}, }, expectedCPE: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Product: "package-name", TargetSW: "npm"}}, }, }, { name: "plain package name", input: DBSearchPackages{ Packages: []string{"package-name"}, }, expectedPkg: v6.PackageSpecifiers{ {Name: "package-name"}, }, expectedCPE: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{Part: "a", Product: "package-name"}}, }, }, { name: "ecosystem without packages", input: DBSearchPackages{ Ecosystem: "npm", }, expectedPkg: v6.PackageSpecifiers{ {Ecosystem: "npm"}, }, expectedCPE: v6.PackageSpecifiers{ {CPE: &cpe.Attributes{TargetSW: "npm"}}, }, }, { name: "conflicting PURL and ecosystem", input: DBSearchPackages{ Packages: []string{"pkg:npm/package-name@1.0.0"}, Ecosystem: "npm", }, expectedErrMsg: "cannot specify both package URL and ecosystem", }, { name: "invalid CPE", input: DBSearchPackages{ Packages: []string{"cpe:2.3:a:$%&^*%"}, }, expectedErrMsg: "invalid CPE", }, { name: "invalid PURL", input: DBSearchPackages{ Packages: []string{"pkg:invalid"}, }, expectedErrMsg: "invalid package URL", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.input.PostLoad() if tc.expectedErrMsg != "" { require.Error(t, err) require.ErrorContains(t, err, tc.expectedErrMsg) return } require.NoError(t, err) if d := cmp.Diff(tc.expectedPkg, tc.input.PkgSpecs); d != "" { t.Errorf("unexpected package specifiers (-want +got):\n%s", d) } if d := cmp.Diff(tc.expectedCPE, tc.input.CPESpecs); d != "" { t.Errorf("unexpected CPE specifiers (-want +got):\n%s", d) } }) } } ================================================ FILE: cmd/grype/cli/options/database_search_vulnerabilities.go ================================================ package options import ( "fmt" "time" "github.com/araddon/dateparse" "github.com/anchore/clio" v6 "github.com/anchore/grype/grype/db/v6" ) type DBSearchVulnerabilities struct { VulnerabilityIDs []string `yaml:"vulnerability-ids" json:"vulnerability-ids" mapstructure:"vulnerability-ids"` UseVulnIDFlag bool `yaml:"-" json:"-" mapstructure:"-"` PublishedAfter string `yaml:"published-after" json:"published-after" mapstructure:"published-after"` ModifiedAfter string `yaml:"modified-after" json:"modified-after" mapstructure:"modified-after"` Providers []string `yaml:"providers" json:"providers" mapstructure:"providers"` FixedState []string `yaml:"fixed-state" json:"fixed-state" mapstructure:"fixed-state"` Specs v6.VulnerabilitySpecifiers `yaml:"-" json:"-" mapstructure:"-"` } func (c *DBSearchVulnerabilities) AddFlags(flags clio.FlagSet) { if c.UseVulnIDFlag { flags.StringArrayVarP(&c.VulnerabilityIDs, "vuln", "", "only show results for the given vulnerability ID") } flags.StringVarP(&c.PublishedAfter, "published-after", "", "only show vulnerabilities originally published after the given date (format: YYYY-MM-DD)") flags.StringVarP(&c.ModifiedAfter, "modified-after", "", "only show vulnerabilities originally published or modified since the given date (format: YYYY-MM-DD)") flags.StringArrayVarP(&c.Providers, "provider", "", "only show vulnerabilities from the given provider") flags.StringArrayVarP(&c.FixedState, "fixed-state", "", "only show vulnerabilities with the given fix state (fixed, not-fixed, unknown, wont-fix)") } func (c *DBSearchVulnerabilities) PostLoad() error { // note: this may be called multiple times, so we need to reset the specs each time c.Specs = nil handleTimeOption := func(val string, flag string) (*time.Time, error) { if val == "" { return nil, nil } parsed, err := dateparse.ParseIn(val, time.UTC) if err != nil { return nil, fmt.Errorf("invalid date format for %s=%q: %w", flag, val, err) } return &parsed, nil } if c.PublishedAfter != "" && c.ModifiedAfter != "" { return fmt.Errorf("only one of --published-after or --modified-after can be set") } validFixStates := map[string]bool{ "fixed": true, "not-fixed": true, "unknown": true, "wont-fix": true, } for _, fs := range c.FixedState { if !validFixStates[fs] { return fmt.Errorf("invalid fixed-state value: %q (valid values: fixed, not-fixed, unknown, wont-fix)", fs) } } var publishedAfter, modifiedAfter *time.Time var err error publishedAfter, err = handleTimeOption(c.PublishedAfter, "published-after") if err != nil { return fmt.Errorf("invalid date format for published-after field: %w", err) } modifiedAfter, err = handleTimeOption(c.ModifiedAfter, "modified-after") if err != nil { return fmt.Errorf("invalid date format for modified-after field: %w", err) } var specs []v6.VulnerabilitySpecifier for _, vulnID := range c.VulnerabilityIDs { specs = append(specs, v6.VulnerabilitySpecifier{ Name: vulnID, PublishedAfter: publishedAfter, ModifiedAfter: modifiedAfter, Providers: c.Providers, }) } if len(specs) == 0 { if c.PublishedAfter != "" || c.ModifiedAfter != "" || len(c.Providers) > 0 { specs = append(specs, v6.VulnerabilitySpecifier{ PublishedAfter: publishedAfter, ModifiedAfter: modifiedAfter, Providers: c.Providers, }) } } c.Specs = specs return nil } ================================================ FILE: cmd/grype/cli/options/database_search_vulnerabilities_test.go ================================================ package options import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" v6 "github.com/anchore/grype/grype/db/v6" ) func TestDBSearchVulnerabilitiesPostLoad(t *testing.T) { testCases := []struct { name string input DBSearchVulnerabilities expectedSpecs v6.VulnerabilitySpecifiers expectedErrMsg string }{ { name: "single vulnerability ID", input: DBSearchVulnerabilities{ VulnerabilityIDs: []string{"CVE-2023-0001"}, }, expectedSpecs: v6.VulnerabilitySpecifiers{ {Name: "CVE-2023-0001"}, }, }, { name: "multiple vulnerability IDs", input: DBSearchVulnerabilities{ VulnerabilityIDs: []string{"CVE-2023-0001", "GHSA-1234"}, }, expectedSpecs: v6.VulnerabilitySpecifiers{ {Name: "CVE-2023-0001"}, {Name: "GHSA-1234"}, }, }, { name: "published-after set", input: DBSearchVulnerabilities{ PublishedAfter: "2023-01-01", }, expectedSpecs: v6.VulnerabilitySpecifiers{ {PublishedAfter: parseTime("2023-01-01")}, }, }, { name: "modified-after set", input: DBSearchVulnerabilities{ ModifiedAfter: "2023-02-01", }, expectedSpecs: v6.VulnerabilitySpecifiers{ {ModifiedAfter: parseTime("2023-02-01")}, }, }, { name: "both published-after and modified-after set", input: DBSearchVulnerabilities{ PublishedAfter: "2023-01-01", ModifiedAfter: "2023-02-01", }, expectedErrMsg: "only one of --published-after or --modified-after can be set", }, { name: "invalid date for published-after", input: DBSearchVulnerabilities{ PublishedAfter: "invalid-date", }, expectedErrMsg: "invalid date format for published-after", }, { name: "invalid date for modified-after", input: DBSearchVulnerabilities{ ModifiedAfter: "invalid-date", }, expectedErrMsg: "invalid date format for modified-after", }, { name: "vulnerability ID with providers", input: DBSearchVulnerabilities{ VulnerabilityIDs: []string{"CVE-2023-0001"}, Providers: []string{"provider1"}, }, expectedSpecs: v6.VulnerabilitySpecifiers{ {Name: "CVE-2023-0001", Providers: []string{"provider1"}}, }, }, { name: "providers without vulnerability IDs", input: DBSearchVulnerabilities{ Providers: []string{"provider1", "provider2"}, }, expectedSpecs: v6.VulnerabilitySpecifiers{ {Providers: []string{"provider1", "provider2"}}, }, }, { name: "valid fixed-state: fixed", input: DBSearchVulnerabilities{ FixedState: []string{"fixed"}, }, expectedSpecs: nil, }, { name: "valid fixed-state: multiple values", input: DBSearchVulnerabilities{ FixedState: []string{"fixed", "not-fixed", "wont-fix", "unknown"}, }, expectedSpecs: nil, }, { name: "invalid fixed-state", input: DBSearchVulnerabilities{ FixedState: []string{"invalid-state"}, }, expectedErrMsg: "invalid fixed-state value: \"invalid-state\"", }, { name: "mixed valid and invalid fixed-state", input: DBSearchVulnerabilities{ FixedState: []string{"fixed", "bad-state"}, }, expectedErrMsg: "invalid fixed-state value: \"bad-state\"", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.input.PostLoad() if tc.expectedErrMsg != "" { require.Error(t, err) require.ErrorContains(t, err, tc.expectedErrMsg) return } require.NoError(t, err) if d := cmp.Diff(tc.expectedSpecs, tc.input.Specs); d != "" { t.Errorf("unexpected vulnerability specifiers (-want +got):\n%s", d) } }) } } func parseTime(value string) *time.Time { t, _ := time.Parse("2006-01-02", value) return &t } ================================================ FILE: cmd/grype/cli/options/datasources.go ================================================ package options import ( "time" "github.com/anchore/clio" "github.com/anchore/grype/grype/matcher/java" ) const ( defaultMavenBaseURL = "https://search.maven.org/solrsearch/select" ) type externalSources struct { Enable bool `yaml:"enable" json:"enable" mapstructure:"enable"` Maven maven `yaml:"maven" json:"maven" mapstructure:"maven"` } var _ interface { clio.FieldDescriber } = (*externalSources)(nil) type maven struct { SearchUpstreamBySha1 bool `yaml:"search-upstream" json:"searchUpstreamBySha1" mapstructure:"search-maven-upstream"` BaseURL string `yaml:"base-url" json:"baseUrl" mapstructure:"base-url"` RateLimit time.Duration `yaml:"rate-limit" json:"rateLimit" mapstructure:"rate-limit"` } func defaultExternalSources() externalSources { return externalSources{ Maven: maven{ SearchUpstreamBySha1: true, BaseURL: defaultMavenBaseURL, RateLimit: 300 * time.Millisecond, }, } } func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig { // always respect if global config is disabled smu := cfg.Maven.SearchUpstreamBySha1 if !cfg.Enable { smu = cfg.Enable } return java.ExternalSearchConfig{ SearchMavenUpstream: smu, MavenBaseURL: cfg.Maven.BaseURL, MavenRateLimit: cfg.Maven.RateLimit, } } func (cfg *externalSources) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&cfg.Enable, `enable Grype searching network source for additional information`) descriptions.Add(&cfg.Maven.SearchUpstreamBySha1, `search for Maven artifacts by SHA1`) descriptions.Add(&cfg.Maven.BaseURL, `base URL of the Maven repository to search`) } ================================================ FILE: cmd/grype/cli/options/experimental.go ================================================ package options // Experimental options are opt-in features that are... // ...not stable // ...not yet fully supported // ...not necessarily tested // ...not ready for production use // these may go away at any moment, do not depend on them type Experimental struct { } ================================================ FILE: cmd/grype/cli/options/fix_channels.go ================================================ package options import ( "fmt" "strings" "github.com/anchore/clio" "github.com/anchore/grype/grype/distro" ) type FixChannelEnabled string type FixChannels struct { // TODO: in the future we may want to support more channels, as well as have a default-apply value here that can be overridden within each channel configuration // EUS is the Extended Update Support channel for RHEL RedHatEUS FixChannel `yaml:"redhat-eus" json:"redhat-eus" mapstructure:"redhat-eus"` } type FixChannel struct { // Apply indicates how the channel should be applied to the distro Apply string `yaml:"apply" json:"apply" mapstructure:"apply"` // Versions specifies a constraint string indicating which versions of the distro this channel applies to (e.g. ">= 8.0" for RHEL 8 and above) Versions string `yaml:"versions" json:"versions" mapstructure:"versions"` } func (o *FixChannel) PostLoad() error { if o.Apply == "" { o.Apply = string(distro.ChannelConditionallyEnabled) } switch strings.ToLower(o.Apply) { case string(distro.ChannelNeverEnabled), string(distro.ChannelAlwaysEnabled), string(distro.ChannelConditionallyEnabled): return nil default: return fmt.Errorf("apply %q valid values are 'never', 'always', or 'auto' (conditionally applied based on SBOM data)", o.Apply) } } func DefaultFixChannels() FixChannels { rhelEUS := distro.DefaultFixChannels().Get("eus") if rhelEUS == nil { panic("default fix channels do not contain Red Hat EUS channel") } // use API defaults for the CLI configuration return FixChannels{ RedHatEUS: FixChannel{ Apply: string(rhelEUS.Apply), Versions: rhelEUS.Versions.Value(), }, } } func (o *FixChannels) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&o.RedHatEUS, `whether to always enable, disable, or automatically detect when to use Red Hat Extended Update Support (EUS) vulnerability data`) descriptions.Add(&o.RedHatEUS.Apply, `whether fixes from this channel should be considered, options are "never", "always", or "auto" (conditionally applied based on SBOM data)`) } ================================================ FILE: cmd/grype/cli/options/grype.go ================================================ package options import ( "fmt" "strings" "github.com/anchore/clio" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/format" "github.com/anchore/syft/syft/source" ) type Grype struct { Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, = the Presenter hint string to use for report formatting and the output file File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to Pretty bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"` Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document) OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix OnlyNotFixed bool `yaml:"only-notfixed" json:"only-notfixed" mapstructure:"only-notfixed"` // only fail if detected vulns don't have a fix IgnoreStates string `yaml:"ignore-states" json:"ignore-wontfix" mapstructure:"ignore-wontfix"` // ignore detections for vulnerabilities matching these comma-separated fix states Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image Search search `yaml:"search" json:"search" mapstructure:"search"` Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"` Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` ExternalSources externalSources `yaml:"external-sources" json:"externalSources" mapstructure:"external-sources"` Match matchConfig `yaml:"match" json:"match" mapstructure:"match"` FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"` Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` ShowSuppressed bool `yaml:"show-suppressed" json:"show-suppressed" mapstructure:"show-suppressed"` ByCVE bool `yaml:"by-cve" json:"by-cve" mapstructure:"by-cve"` // --by-cve, indicates if the original match vulnerability IDs should be preserved or the CVE should be used instead SortBy SortBy `yaml:",inline" json:",inline" mapstructure:",squash"` Name string `yaml:"name" json:"name" mapstructure:"name"` DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` From []string `yaml:"from" json:"from" mapstructure:"from"` VexDocuments []string `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"` VexAdd []string `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"` // GRYPE_VEX_ADD MatchUpstreamKernelHeaders bool `yaml:"match-upstream-kernel-headers" json:"match-upstream-kernel-headers" mapstructure:"match-upstream-kernel-headers"` // Show matches on kernel-headers packages where the match is on kernel upstream instead of marking them as ignored, default=false FixChannel FixChannels `yaml:"fix-channel" json:"fix-channel" mapstructure:"fix-channel"` // the fix channels to apply to the distro when matching Timestamp bool `yaml:"timestamp" json:"timestamp" mapstructure:"timestamp"` Alerts Alerts `yaml:"alerts" json:"alerts" mapstructure:"alerts"` DatabaseCommand `yaml:",inline" json:",inline" mapstructure:",squash"` } type developer struct { DB databaseDeveloper `yaml:"db" json:"db" mapstructure:"db"` } type databaseDeveloper struct { Debug bool `yaml:"debug" json:"debug" mapstructure:"debug"` } var _ interface { clio.FlagAdder clio.PostLoader clio.FieldDescriber } = (*Grype)(nil) func DefaultGrype(id clio.Identification) *Grype { return &Grype{ Search: defaultSearch(source.SquashedScope), FixChannel: DefaultFixChannels(), DatabaseCommand: DatabaseCommand{ DB: DefaultDatabase(id), }, Match: defaultMatchConfig(), ExternalSources: defaultExternalSources(), CheckForAppUpdate: true, VexAdd: []string{}, MatchUpstreamKernelHeaders: false, SortBy: defaultSortBy(), Timestamp: true, Alerts: defaultAlerts(), } } // nolint:funlen func (o *Grype) AddFlags(flags clio.FlagSet) { flags.StringVarP(&o.Search.Scope, "scope", "s", fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes), ) flags.StringArrayVarP(&o.Outputs, "output", "o", fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", format.AvailableFormats, format.DeprecatedFormats), ) flags.StringVarP(&o.File, "file", "", "file to write the default report output to (default is STDOUT)", ) flags.StringVarP(&o.Name, "name", "", "set the name of the target being analyzed", ) flags.StringVarP(&o.Distro, "distro", "", "distro to match against in the format: [-:@]", ) flags.BoolVarP(&o.GenerateMissingCPEs, "add-cpes-if-none", "", "generate CPEs for packages with no CPE data", ) flags.StringVarP(&o.OutputTemplateFile, "template", "t", "specify the path to a Go template file (requires 'template' output to be selected)") flags.StringVarP(&o.FailOn, "fail-on", "f", fmt.Sprintf("set the return code to 2 if a vulnerability is found with a severity >= the given severity, options=%v", vulnerability.AllSeverities()), ) flags.BoolVarP(&o.OnlyFixed, "only-fixed", "", "ignore matches for vulnerabilities that are not fixed", ) flags.BoolVarP(&o.OnlyNotFixed, "only-notfixed", "", "ignore matches for vulnerabilities that are fixed", ) flags.StringVarP(&o.IgnoreStates, "ignore-states", "", fmt.Sprintf("ignore matches for vulnerabilities with specified comma separated fix states, options=%v", vulnerability.AllFixStates()), ) flags.BoolVarP(&o.ByCVE, "by-cve", "", "orient results by CVE instead of the original vulnerability ID when possible", ) flags.BoolVarP(&o.ShowSuppressed, "show-suppressed", "", "show suppressed/ignored vulnerabilities in the output (only supported with table output format)", ) flags.StringArrayVarP(&o.Exclusions, "exclude", "", "exclude paths from being scanned using a glob expression", ) flags.StringVarP(&o.Platform, "platform", "", "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", ) flags.StringArrayVarP(&o.From, "from", "", "specify the source behavior to use (e.g. docker, registry, podman, oci-dir, ...)", ) flags.StringArrayVarP(&o.VexDocuments, "vex", "", "a list of VEX documents to consider when producing scanning results", ) } func (o *Grype) PostLoad() error { o.From = flatten(o.From) if o.FailOn != "" { failOnSeverity := *o.FailOnSeverity() if failOnSeverity == vulnerability.UnknownSeverity { return fmt.Errorf("bad --fail-on severity value '%s'", o.FailOn) } } return nil } func (o *Grype) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&o.CheckForAppUpdate, `enable/disable checking for application updates on startup`) descriptions.Add(&o.DefaultImagePullSource, `allows users to specify which image source should be used to generate the sbom valid values are: registry, docker, podman`) descriptions.Add(&o.Name, `same as --name; set the name of the target being analyzed`) descriptions.Add(&o.Exclusions, `a list of globs to exclude from scanning, for example: - '/etc/**' - './out/**/*.json' same as --exclude`) descriptions.Add(&o.File, `if using template output, you must provide a path to a Go template file see https://github.com/anchore/grype#using-templates for more information on template output the default path to the template file is the current working directory output-template-file: .grype/html.tmpl write output report to a file (default is to write to stdout)`) descriptions.Add(&o.Outputs, `the output format of the vulnerability report (options: table, template, json, cyclonedx) when using template as the output type, you must also provide a value for 'output-template-file'`) descriptions.Add(&o.Pretty, `pretty-print output`) descriptions.Add(&o.FailOn, `upon scanning, if a severity is found at or above the given severity then the return code will be 1 default is unset which will skip this validation (options: negligible, low, medium, high, critical)`) descriptions.Add(&o.Ignore, `A list of vulnerability ignore rules, one or more property may be specified and all matching vulnerabilities will be ignored. This is the full set of supported rule fields: - vulnerability: CVE-2008-4318 fix-state: unknown package: name: libcurl version: 1.5.1 type: npm location: "/usr/local/lib/node_modules/**" VEX fields apply when Grype reads vex data: - vex-status: not_affected vex-justification: vulnerable_code_not_present `) descriptions.Add(&o.VexAdd, `VEX statuses to consider as ignored rules`) descriptions.Add(&o.MatchUpstreamKernelHeaders, `match kernel-header packages with upstream kernel as kernel vulnerabilities`) } func (o Grype) FailOnSeverity() *vulnerability.Severity { severity := vulnerability.ParseSeverity(o.FailOn) return &severity } // flatten takes a list of comma-separated entries and returns a flattened list of trimmed values (preserving order) func flatten(commaSeparatedEntries []string) []string { var out []string for _, v := range commaSeparatedEntries { for _, s := range strings.Split(v, ",") { out = append(out, strings.TrimSpace(s)) } } return out } ================================================ FILE: cmd/grype/cli/options/grype_test.go ================================================ package options import ( "testing" "github.com/stretchr/testify/assert" ) func Test_flatten(t *testing.T) { tests := []struct { name string input []string expected []string }{ { name: "single value", input: []string{"docker"}, expected: []string{"docker"}, }, { name: "comma-separated values", input: []string{"docker,registry"}, expected: []string{"docker", "registry"}, }, { name: "multiple entries with commas", input: []string{"docker,registry", "podman"}, expected: []string{"docker", "registry", "podman"}, // preserves order }, { name: "whitespace trimming", input: []string{" docker , registry "}, expected: []string{"docker", "registry"}, }, { name: "empty input", input: []string{}, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := flatten(tt.input) assert.Equal(t, tt.expected, got) }) } } ================================================ FILE: cmd/grype/cli/options/match.go ================================================ package options import ( "fmt" "github.com/anchore/clio" "github.com/anchore/grype/grype/version" ) // matchConfig contains all matching-related configuration options available to the user via the application config. type matchConfig struct { Java matcherConfig `yaml:"java" json:"java" mapstructure:"java"` // settings for the java matcher JVM matcherConfig `yaml:"jvm" json:"jvm" mapstructure:"jvm"` // settings for the jvm matcher Dotnet matcherConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` // settings for the dotnet matcher Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"` // settings for the golang matcher Javascript matcherConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"` // settings for the javascript matcher Python matcherConfig `yaml:"python" json:"python" mapstructure:"python"` // settings for the python matcher Ruby matcherConfig `yaml:"ruby" json:"ruby" mapstructure:"ruby"` // settings for the ruby matcher Rust matcherConfig `yaml:"rust" json:"rust" mapstructure:"rust"` // settings for the rust matcher Hex matcherConfig `yaml:"hex" json:"hex" mapstructure:"hex"` // settings for the hex matcher (Elixir/Erlang) Stock matcherConfig `yaml:"stock" json:"stock" mapstructure:"stock"` // settings for the default/stock matcher Dpkg dpkgConfig `yaml:"dpkg" json:"dpkg" mapstructure:"dpkg"` // settings for the dpkg matcher Rpm rpmConfig `yaml:"rpm" json:"rpm" mapstructure:"rpm"` // settings for the rpm matcher } var _ interface { clio.FieldDescriber clio.PostLoader } = (*matchConfig)(nil) type matcherConfig struct { UseCPEs bool `yaml:"using-cpes" json:"using-cpes" mapstructure:"using-cpes"` // if CPEs should be used during matching } type golangConfig struct { matcherConfig `yaml:",inline" mapstructure:",squash"` AlwaysUseCPEForStdlib bool `yaml:"always-use-cpe-for-stdlib" json:"always-use-cpe-for-stdlib" mapstructure:"always-use-cpe-for-stdlib"` // if CPEs should be used during matching AllowMainModulePseudoVersionComparison bool `yaml:"allow-main-module-pseudo-version-comparison" json:"allow-main-module-pseudo-version-comparison" mapstructure:"allow-main-module-pseudo-version-comparison"` // if pseudo versions should be compared } // dpkgConfig contains configuration for the dpkg matcher. type dpkgConfig struct { matcherConfig `yaml:",inline" mapstructure:",squash"` // MissingEpochStrategy controls how missing epochs in package versions are handled // during vulnerability matching. // // Valid values: // - "zero" (default): Treat missing epochs as 0 // - "auto": Assume missing epoch matches the constraint's epoch // // The "zero" strategy follows dpkg specification guidance and maintains backward // compatibility with existing Grype behavior. The "auto" strategy reduces false // positives by recognizing that distros rarely track multiple epochs of the same // package in the same release. // // Example: // Package version: 2.0.0 (no epoch) // Constraint: < 1:1.5.0 (epoch 1) // // With "zero": Treat package as 0:2.0.0 → MATCH (0 < 1) // With "auto": Treat package as 1:2.0.0 → NO MATCH (2.0.0 > 1.5.0) MissingEpochStrategy version.MissingEpochStrategy `yaml:"missing-epoch-strategy" json:"missing-epoch-strategy" mapstructure:"missing-epoch-strategy"` UseCPEsForEOL bool `yaml:"use-cpes-for-eol" json:"use-cpes-for-eol" mapstructure:"use-cpes-for-eol"` // if CPEs should be used for EOL distro packages } // rpmConfig contains configuration for the RPM matcher. type rpmConfig struct { matcherConfig `yaml:",inline" mapstructure:",squash"` // MissingEpochStrategy controls how missing epochs in package versions are handled // during vulnerability matching. // // Valid values: // - "zero" (default): Treat missing epochs as 0 // - "auto": Assume missing epoch matches the constraint's epoch // // The "zero" strategy follows RPM specification guidance and maintains backward // compatibility with existing Grype behavior. The "auto" strategy reduces false // positives by recognizing that distros rarely track multiple epochs of the same // package in the same release. // // Example: // Package version: 2.0.0 (no epoch) // Constraint: < 1:1.5.0 (epoch 1) // // With "zero": Treat package as 0:2.0.0 → MATCH (0 < 1) // With "auto": Treat package as 1:2.0.0 → NO MATCH (2.0.0 > 1.5.0) MissingEpochStrategy version.MissingEpochStrategy `yaml:"missing-epoch-strategy" json:"missing-epoch-strategy" mapstructure:"missing-epoch-strategy"` UseCPEsForEOL bool `yaml:"use-cpes-for-eol" json:"use-cpes-for-eol" mapstructure:"use-cpes-for-eol"` // if CPEs should be used for EOL distro packages } func defaultGolangConfig() golangConfig { return golangConfig{ matcherConfig: matcherConfig{ UseCPEs: false, }, AlwaysUseCPEForStdlib: true, AllowMainModulePseudoVersionComparison: false, } } func defaultRpmConfig() rpmConfig { return rpmConfig{ matcherConfig: matcherConfig{UseCPEs: false}, MissingEpochStrategy: version.MissingEpochStrategyAuto, UseCPEsForEOL: false, } } func defaultDpkgConfig() dpkgConfig { return dpkgConfig{ matcherConfig: matcherConfig{UseCPEs: false}, MissingEpochStrategy: version.MissingEpochStrategyZero, UseCPEsForEOL: false, } } func defaultMatchConfig() matchConfig { useCpe := matcherConfig{UseCPEs: true} dontUseCpe := matcherConfig{UseCPEs: false} return matchConfig{ Java: dontUseCpe, JVM: useCpe, Dotnet: dontUseCpe, Golang: defaultGolangConfig(), Javascript: dontUseCpe, Python: dontUseCpe, Ruby: dontUseCpe, Rust: dontUseCpe, Hex: dontUseCpe, Stock: useCpe, Dpkg: defaultDpkgConfig(), Rpm: defaultRpmConfig(), } } func (cfg *matchConfig) PostLoad() error { if err := cfg.Rpm.PostLoad(); err != nil { return err } if err := cfg.Dpkg.PostLoad(); err != nil { return err } return nil } // PostLoad validates the RPM configuration. func (cfg *rpmConfig) PostLoad() error { if cfg.MissingEpochStrategy != version.MissingEpochStrategyZero && cfg.MissingEpochStrategy != version.MissingEpochStrategyAuto { return fmt.Errorf("invalid rpm.missing-epoch-strategy: %q (allowable: %s, %s)", cfg.MissingEpochStrategy, version.MissingEpochStrategyZero, version.MissingEpochStrategyAuto) } return nil } // PostLoad validates the dpkg configuration. func (cfg *dpkgConfig) PostLoad() error { if cfg.MissingEpochStrategy != version.MissingEpochStrategyZero && cfg.MissingEpochStrategy != version.MissingEpochStrategyAuto { return fmt.Errorf("invalid dpkg.missing-epoch-strategy: %q (allowable: %s, %s)", cfg.MissingEpochStrategy, version.MissingEpochStrategyZero, version.MissingEpochStrategyAuto) } return nil } func (cfg *matchConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { usingCpeDescription := `use CPE matching to find vulnerabilities` descriptions.Add(&cfg.Java.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Dotnet.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Golang.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Golang.AlwaysUseCPEForStdlib, usingCpeDescription+" for the Go standard library") descriptions.Add(&cfg.Golang.AllowMainModulePseudoVersionComparison, `allow comparison between main module pseudo-versions (e.g. v0.0.0-20240413-2b432cf643...)`) descriptions.Add(&cfg.Javascript.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Python.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Ruby.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Rust.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Hex.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Stock.UseCPEs, usingCpeDescription) descriptions.Add(&cfg.Dpkg.MissingEpochStrategy, `strategy for handling missing epochs in dpkg package versions during matching (options: zero, auto)`) descriptions.Add(&cfg.Rpm.MissingEpochStrategy, `strategy for handling missing epochs in RPM package versions during matching (options: zero, auto)`) eolCpeDescription := `use CPE matching for packages from end-of-life distributions` descriptions.Add(&cfg.Dpkg.UseCPEsForEOL, eolCpeDescription) descriptions.Add(&cfg.Rpm.UseCPEsForEOL, eolCpeDescription) } ================================================ FILE: cmd/grype/cli/options/match_test.go ================================================ package options import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/version" ) func TestRpmConfig_PostLoad(t *testing.T) { tests := []struct { name string cfg rpmConfig wantErr bool errMsg string }{ { name: "valid zero strategy", cfg: rpmConfig{MissingEpochStrategy: "zero"}, wantErr: false, }, { name: "valid auto strategy", cfg: rpmConfig{MissingEpochStrategy: "auto"}, wantErr: false, }, { name: "invalid strategy", cfg: rpmConfig{MissingEpochStrategy: "garbage"}, wantErr: true, errMsg: `invalid rpm.missing-epoch-strategy: "garbage" (allowable: zero, auto)`, }, { name: "empty strategy fails validation", cfg: rpmConfig{MissingEpochStrategy: ""}, wantErr: true, errMsg: `invalid rpm.missing-epoch-strategy: "" (allowable: zero, auto)`, }, { name: "case sensitive - Zero is invalid", cfg: rpmConfig{MissingEpochStrategy: "Zero"}, wantErr: true, }, { name: "case sensitive - AUTO is invalid", cfg: rpmConfig{MissingEpochStrategy: "AUTO"}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.PostLoad() if tt.wantErr { require.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { require.NoError(t, err) } }) } } func TestDpkgConfig_PostLoad(t *testing.T) { tests := []struct { name string cfg dpkgConfig wantErr bool errMsg string }{ { name: "valid zero strategy", cfg: dpkgConfig{MissingEpochStrategy: "zero"}, wantErr: false, }, { name: "valid auto strategy", cfg: dpkgConfig{MissingEpochStrategy: "auto"}, wantErr: false, }, { name: "invalid strategy", cfg: dpkgConfig{MissingEpochStrategy: "invalid"}, wantErr: true, errMsg: `invalid dpkg.missing-epoch-strategy: "invalid" (allowable: zero, auto)`, }, { name: "empty strategy fails validation", cfg: dpkgConfig{MissingEpochStrategy: ""}, wantErr: true, errMsg: `invalid dpkg.missing-epoch-strategy: "" (allowable: zero, auto)`, }, { name: "whitespace strategy is invalid", cfg: dpkgConfig{MissingEpochStrategy: " zero "}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.PostLoad() if tt.wantErr { require.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { require.NoError(t, err) } }) } } func TestMatchConfig_PostLoad(t *testing.T) { tests := []struct { name string cfg matchConfig wantErr bool errMsg string }{ { name: "valid rpm and dpkg configs", cfg: matchConfig{ Rpm: rpmConfig{MissingEpochStrategy: "zero"}, Dpkg: dpkgConfig{MissingEpochStrategy: "auto"}, }, wantErr: false, }, { name: "invalid rpm config", cfg: matchConfig{ Rpm: rpmConfig{MissingEpochStrategy: "bad"}, Dpkg: dpkgConfig{MissingEpochStrategy: "zero"}, }, wantErr: true, errMsg: "rpm.missing-epoch-strategy", }, { name: "invalid dpkg config", cfg: matchConfig{ Rpm: rpmConfig{MissingEpochStrategy: "zero"}, Dpkg: dpkgConfig{MissingEpochStrategy: "bad"}, }, wantErr: true, errMsg: "dpkg.missing-epoch-strategy", }, { name: "both invalid", cfg: matchConfig{ Rpm: rpmConfig{MissingEpochStrategy: "bad"}, Dpkg: dpkgConfig{MissingEpochStrategy: "bad"}, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.PostLoad() if tt.wantErr { require.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { require.NoError(t, err) } }) } } func TestDefaultRpmConfig(t *testing.T) { cfg := defaultRpmConfig() assert.Equal(t, version.MissingEpochStrategyAuto, cfg.MissingEpochStrategy, "default should be auto for backward compatibility (RPM ignores one-sided epochs)") assert.False(t, cfg.UseCPEs, "rpm matcher should not use CPEs by default") // Ensure default is valid err := cfg.PostLoad() require.NoError(t, err, "default config should be valid") } func TestDefaultDpkgConfig(t *testing.T) { cfg := defaultDpkgConfig() assert.Equal(t, version.MissingEpochStrategyZero, cfg.MissingEpochStrategy, "default should be zero for backward compatibility") assert.False(t, cfg.UseCPEs, "dpkg matcher should not use CPEs by default") // Ensure default is valid err := cfg.PostLoad() require.NoError(t, err, "default config should be valid") } func TestDefaultMatchConfig(t *testing.T) { cfg := defaultMatchConfig() // Verify RPM defaults (auto preserves legacy behavior where one-sided epochs are ignored) assert.Equal(t, version.MissingEpochStrategyAuto, cfg.Rpm.MissingEpochStrategy) assert.False(t, cfg.Rpm.UseCPEs) // Verify dpkg defaults (zero preserves legacy behavior where missing epoch = 0) assert.Equal(t, version.MissingEpochStrategyZero, cfg.Dpkg.MissingEpochStrategy) assert.False(t, cfg.Dpkg.UseCPEs) // Ensure the entire default config is valid err := cfg.PostLoad() require.NoError(t, err, "default match config should be valid") } ================================================ FILE: cmd/grype/cli/options/registry.go ================================================ package options import ( "os" "github.com/anchore/clio" "github.com/anchore/stereoscope/pkg/image" ) type RegistryCredentials struct { Authority string `yaml:"authority" json:"authority" mapstructure:"authority"` // IMPORTANT: do not show the username, password, or token in any output (sensitive information) Username secret `yaml:"username" json:"username" mapstructure:"username"` Password secret `yaml:"password" json:"password" mapstructure:"password"` Token secret `yaml:"token" json:"token" mapstructure:"token"` TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"` TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"` } type registry struct { InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"` Auth []RegistryCredentials `yaml:"auth" json:"auth,omitempty" mapstructure:"auth"` CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` } var _ interface { clio.PostLoader clio.FieldDescriber } = (*registry)(nil) func (cfg *registry) PostLoad() error { // there may be additional credentials provided by env var that should be appended to the set of credentials authority, username, password, token, tlsCert, tlsKey := os.Getenv("GRYPE_REGISTRY_AUTH_AUTHORITY"), os.Getenv("GRYPE_REGISTRY_AUTH_USERNAME"), os.Getenv("GRYPE_REGISTRY_AUTH_PASSWORD"), os.Getenv("GRYPE_REGISTRY_AUTH_TOKEN"), os.Getenv("GRYPE_REGISTRY_AUTH_TLS_CERT"), os.Getenv("GRYPE_REGISTRY_AUTH_TLS_KEY") if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) { // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration. cfg.Auth = append([]RegistryCredentials{ { Authority: authority, Username: secret(username), Password: secret(password), Token: secret(token), TLSCert: tlsCert, TLSKey: tlsKey, }, }, cfg.Auth...) } return nil } func (cfg *registry) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&cfg.InsecureSkipTLSVerify, "skip TLS verification when communicating with the registry") descriptions.Add(&cfg.InsecureUseHTTP, "use http instead of https when connecting to the registry") descriptions.Add(&cfg.CACert, "filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate") descriptions.Add(&cfg.Auth, `Authentication credentials for specific registries. Each entry describes authentication for a specific authority: - authority: the registry authority URL the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) (env: SYFT_REGISTRY_AUTH_AUTHORITY) username: a username if using basic credentials (env: SYFT_REGISTRY_AUTH_USERNAME) password: a corresponding password (env: SYFT_REGISTRY_AUTH_PASSWORD) token: a token if using token-based authentication, mutually exclusive with username/password (env: SYFT_REGISTRY_AUTH_TOKEN) tls-cert: filepath to the client certificate used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_CERT) tls-key: filepath to the client key used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_KEY) `) } func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool { hasUserPass := username != "" && password != "" hasToken := token != "" hasTLSMaterial := tlsCert != "" && tlsKey != "" return hasUserPass || hasToken || hasTLSMaterial } func (cfg *registry) ToOptions() *image.RegistryOptions { var auth = make([]image.RegistryCredentials, len(cfg.Auth)) for i, a := range cfg.Auth { auth[i] = image.RegistryCredentials{ Authority: a.Authority, Username: string(a.Username), Password: string(a.Password), Token: string(a.Token), ClientCert: a.TLSCert, ClientKey: a.TLSKey, } } return &image.RegistryOptions{ InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify, InsecureUseHTTP: cfg.InsecureUseHTTP, Credentials: auth, CAFileOrDir: cfg.CACert, } } ================================================ FILE: cmd/grype/cli/options/registry_test.go ================================================ package options import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/anchore/stereoscope/pkg/image" ) func TestHasNonEmptyCredentials(t *testing.T) { tests := []struct { username, password, token, cert, key string expected bool }{ { "", "", "", "", "", false, }, { "user", "", "", "", "", false, }, { "", "pass", "", "", "", false, }, { "", "pass", "tok", "", "", true, }, { "user", "", "tok", "", "", true, }, { "", "", "tok", "", "", true, }, { "user", "pass", "tok", "", "", true, }, { "user", "pass", "", "", "", true, }, { "", "", "", "cert", "key", true, }, { "", "", "", "cert", "", false, }, { "", "", "", "", "key", false, }, } for _, test := range tests { t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) { assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key)) }) } } func Test_registry_ToOptions(t *testing.T) { tests := []struct { name string input registry expected image.RegistryOptions }{ { name: "no registry options", input: registry{}, expected: image.RegistryOptions{ Credentials: []image.RegistryCredentials{}, }, }, { name: "set InsecureSkipTLSVerify", input: registry{ InsecureSkipTLSVerify: true, }, expected: image.RegistryOptions{ InsecureSkipTLSVerify: true, Credentials: []image.RegistryCredentials{}, }, }, { name: "set InsecureUseHTTP", input: registry{ InsecureUseHTTP: true, }, expected: image.RegistryOptions{ InsecureUseHTTP: true, Credentials: []image.RegistryCredentials{}, }, }, { name: "set all bool options", input: registry{ InsecureSkipTLSVerify: true, InsecureUseHTTP: true, }, expected: image.RegistryOptions{ InsecureSkipTLSVerify: true, InsecureUseHTTP: true, Credentials: []image.RegistryCredentials{}, }, }, { name: "provide all tls configuration", input: registry{ CACert: "ca.crt", InsecureSkipTLSVerify: true, Auth: []RegistryCredentials{ { TLSCert: "client.crt", TLSKey: "client.key", }, }, }, expected: image.RegistryOptions{ CAFileOrDir: "ca.crt", InsecureSkipTLSVerify: true, Credentials: []image.RegistryCredentials{ { ClientCert: "client.crt", ClientKey: "client.key", }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert.Equal(t, &test.expected, test.input.ToOptions()) }) } } ================================================ FILE: cmd/grype/cli/options/search.go ================================================ package options import ( "fmt" "github.com/anchore/clio" "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/source" ) type search struct { Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` IncludeUnindexedArchives bool `yaml:"unindexed-archives" json:"unindexed-archives" mapstructure:"unindexed-archives"` IncludeIndexedArchives bool `yaml:"indexed-archives" json:"indexed-archives" mapstructure:"indexed-archives"` } var _ interface { clio.PostLoader clio.FieldDescriber } = (*search)(nil) func defaultSearch(scope source.Scope) search { c := cataloging.DefaultArchiveSearchConfig() return search{ Scope: scope.String(), IncludeUnindexedArchives: c.IncludeUnindexedArchives, IncludeIndexedArchives: c.IncludeIndexedArchives, } } func (cfg *search) PostLoad() error { scopeOption := cfg.GetScope() if scopeOption == source.UnknownScope { return fmt.Errorf("bad scope value %q", cfg.Scope) } return nil } func (cfg *search) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&cfg.IncludeIndexedArchives, `search within archives that do contain a file index to search against (zip) note: for now this only applies to the java package cataloger`) descriptions.Add(&cfg.IncludeUnindexedArchives, `search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc) note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed note: for now this only applies to the java package cataloger`) } func (cfg search) GetScope() source.Scope { return source.ParseScope(cfg.Scope) } ================================================ FILE: cmd/grype/cli/options/secret.go ================================================ package options import ( "fmt" "github.com/anchore/clio" "github.com/anchore/grype/internal/redact" ) type secret string var _ interface { fmt.Stringer clio.PostLoader } = (*secret)(nil) // PostLoad needs to use a pointer receiver, even if it's not modifying the value func (r *secret) PostLoad() error { redact.Add(string(*r)) return nil } func (r secret) String() string { if r == "" { return "" } // match the redactor's behavior, replacing with 7 asterisks return "*******" } func (r secret) MarshalText() ([]byte, error) { return []byte(r.String()), nil } ================================================ FILE: cmd/grype/cli/options/sort_by.go ================================================ package options import ( "fmt" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/clio" "github.com/anchore/fangs" "github.com/anchore/grype/grype/presenter/models" ) var _ interface { fangs.FlagAdder fangs.PostLoader } = (*SortBy)(nil) type SortBy struct { Criteria string `yaml:"sort-by" json:"sort-by" mapstructure:"sort-by"` AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"` } func defaultSortBy() SortBy { var strategies []string for _, s := range models.SortStrategies() { strategies = append(strategies, strings.ToLower(s.String())) } return SortBy{ Criteria: models.DefaultSortStrategy.String(), AllowableOptions: strategies, } } func (o *SortBy) AddFlags(flags clio.FlagSet) { flags.StringVarP(&o.Criteria, "sort-by", "", fmt.Sprintf("sort the match results with the given strategy, options=%v", o.AllowableOptions), ) } func (o *SortBy) PostLoad() error { if !strset.New(o.AllowableOptions...).Has(strings.ToLower(o.Criteria)) { return fmt.Errorf("invalid sort-by criteria: %q (allowable: %s)", o.Criteria, strings.Join(o.AllowableOptions, ", ")) } return nil } ================================================ FILE: cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap ================================================ [TestHandler_handleDatabaseDiffStarted/DB_diff_started - 1] ⠋ Comparing Vulnerability DBs ━━━━━━━━━━━━━━━━━━━━ [current] --- [TestHandler_handleDatabaseDiffStarted/DB_diff_complete - 1] ✔ Compared Vulnerability DBs [20 differences found] --- ================================================ FILE: cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap ================================================ [TestHandler_handleUpdateVulnerabilityDatabase/downloading_DB - 1] ⠋ Vulnerability DB ━━━━━━━━━━━━━━━━━━━━ [current] --- [TestHandler_handleUpdateVulnerabilityDatabase/DB_download_complete - 1] ✔ Vulnerability DB [current] --- ================================================ FILE: cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap ================================================ [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/task_line - 1] ⠋ Scanning for vulnerabilities [36 vulnerability matches] --- [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/tree - 1] ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) └── by status: 30 fixed, 10 not-fixed, 4 ignored (2 dropped) --- [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/task_line - 1] ✔ Scanned for vulnerabilities [40 vulnerability matches] --- [TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/tree - 1] ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) └── by status: 35 fixed, 10 not-fixed, 5 ignored (3 dropped) --- ================================================ FILE: cmd/grype/cli/ui/handle_database_diff_started.go ================================================ package ui import ( "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/internal/log" ) type dbDiffProgressStager struct { monitor *monitor.DBDiff } func (p dbDiffProgressStager) Stage() string { if progress.IsErrCompleted(p.monitor.StageProgress.Error()) { return fmt.Sprintf("%d differences found", p.monitor.DifferencesDiscovered.Current()) } return p.monitor.Stager.Stage() } func (p dbDiffProgressStager) Current() int64 { return p.monitor.StageProgress.Current() } func (p dbDiffProgressStager) Error() error { return p.monitor.StageProgress.Error() } func (p dbDiffProgressStager) Size() int64 { return p.monitor.StageProgress.Size() } func (m *Handler) handleDatabaseDiffStarted(e partybus.Event) ([]tea.Model, tea.Cmd) { mon, err := parsers.ParseDatabaseDiffingStarted(e) if err != nil { log.WithFields("error", err).Warn("unable to parse event") return nil, nil } tsk := m.newTaskProgress( taskprogress.Title{ Default: "Compare Vulnerability DBs", Running: "Comparing Vulnerability DBs", Success: "Compared Vulnerability DBs", }, taskprogress.WithStagedProgressable(dbDiffProgressStager{monitor: mon}), ) tsk.HideStageOnSuccess = false return []tea.Model{tsk}, nil } ================================================ FILE: cmd/grype/cli/ui/handle_database_diff_started_test.go ================================================ package ui import ( "testing" "time" tea "github.com/charmbracelet/bubbletea" "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" ) func TestHandler_handleDatabaseDiffStarted(t *testing.T) { tests := []struct { name string eventFn func(*testing.T) partybus.Event iterations int }{ { name: "DB diff started", eventFn: func(t *testing.T) partybus.Event { prog := &progress.Manual{} prog.SetTotal(100) prog.Set(50) diffs := &progress.Manual{} diffs.Set(20) mon := monitor.DBDiff{ Stager: &progress.Stage{Current: "current"}, StageProgress: prog, DifferencesDiscovered: diffs, } return partybus.Event{ Type: event.DatabaseDiffingStarted, Value: mon, } }, }, { name: "DB diff complete", eventFn: func(t *testing.T) partybus.Event { prog := &progress.Manual{} prog.SetTotal(100) prog.Set(100) prog.SetCompleted() diffs := &progress.Manual{} diffs.Set(20) mon := monitor.DBDiff{ Stager: &progress.Stage{Current: "current"}, StageProgress: prog, DifferencesDiscovered: diffs, } return partybus.Event{ Type: event.DatabaseDiffingStarted, Value: mon, } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := tt.eventFn(t) handler := New(DefaultHandlerConfig()) handler.WindowSize = tea.WindowSizeMsg{ Width: 100, Height: 80, } models, _ := handler.Handle(e) require.Len(t, models, 1) model := models[0] tsk, ok := model.(taskprogress.Model) require.True(t, ok) got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ Time: time.Now(), Sequence: tsk.Sequence(), ID: tsk.ID(), }) t.Log(got) snaps.MatchSnapshot(t, got) }) } } ================================================ FILE: cmd/grype/cli/ui/handle_update_vulnerability_database.go ================================================ package ui import ( "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/dustin/go-humanize" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/internal/log" ) type dbDownloadProgressStager struct { prog progress.StagedProgressable } func (s dbDownloadProgressStager) Stage() string { stage := s.prog.Stage() if stage == "downloading" { // note: since validation is baked into the download progress there is no visibility into this stage. // for that reason we report "validating" on the last byte being downloaded (which tends to be the longest // since go-downloader is doing this work). if s.prog.Current() >= s.prog.Size()-1 { return "validating" } // show intermediate progress of the download return fmt.Sprintf("%s / %s", humanize.Bytes(safeConvertInt64ToUint64(s.prog.Current())), humanize.Bytes(safeConvertInt64ToUint64(s.prog.Size())), ) } return stage } func (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) ([]tea.Model, tea.Cmd) { prog, err := parsers.ParseUpdateVulnerabilityDatabase(e) if err != nil { log.WithFields("error", err).Warn("unable to parse event") return nil, nil } tsk := m.newTaskProgress( taskprogress.Title{ Default: "Vulnerability DB", }, taskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event taskprogress.WithStager(dbDownloadProgressStager{prog: prog}), ) tsk.HideStageOnSuccess = false return []tea.Model{tsk}, nil } func safeConvertInt64ToUint64(i int64) uint64 { if i < 0 { return 0 } return uint64(i) } ================================================ FILE: cmd/grype/cli/ui/handle_update_vulnerability_database_test.go ================================================ package ui import ( "testing" "time" tea "github.com/charmbracelet/bubbletea" "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event" ) func TestHandler_handleUpdateVulnerabilityDatabase(t *testing.T) { tests := []struct { name string eventFn func(*testing.T) partybus.Event iterations int }{ { name: "downloading DB", eventFn: func(t *testing.T) partybus.Event { prog := &progress.Manual{} prog.SetTotal(100) prog.Set(50) mon := struct { progress.Progressable progress.Stager }{ Progressable: prog, Stager: &progress.Stage{ Current: "current", }, } return partybus.Event{ Type: event.UpdateVulnerabilityDatabase, Value: mon, } }, }, { name: "DB download complete", eventFn: func(t *testing.T) partybus.Event { prog := &progress.Manual{} prog.SetTotal(100) prog.Set(100) prog.SetCompleted() mon := struct { progress.Progressable progress.Stager }{ Progressable: prog, Stager: &progress.Stage{ Current: "current", }, } return partybus.Event{ Type: event.UpdateVulnerabilityDatabase, Value: mon, } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := tt.eventFn(t) handler := New(DefaultHandlerConfig()) handler.WindowSize = tea.WindowSizeMsg{ Width: 100, Height: 80, } models, _ := handler.Handle(e) require.Len(t, models, 1) model := models[0] tsk, ok := model.(taskprogress.Model) require.True(t, ok) got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ Time: time.Now(), Sequence: tsk.Sequence(), ID: tsk.ID(), }) t.Log(got) snaps.MatchSnapshot(t, got) }) } } ================================================ FILE: cmd/grype/cli/ui/handle_vulnerability_scanning_started.go ================================================ package ui import ( "fmt" "sort" "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) const ( branch = "├──" end = "└──" ) var _ progress.StagedProgressable = (*vulnerabilityScanningAdapter)(nil) type vulnerabilityProgressTree struct { mon *monitor.Matching windowSize tea.WindowSizeMsg countBySeverity map[vulnerability.Severity]int64 unknownCount int64 fixedCount int64 ignoredCount int64 droppedCount int64 totalCount int64 severities []vulnerability.Severity id uint32 sequence int updateDuration time.Duration textStyle lipgloss.Style } func newVulnerabilityProgressTree(monitor *monitor.Matching, textStyle lipgloss.Style) vulnerabilityProgressTree { allSeverities := vulnerability.AllSeverities() sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) return vulnerabilityProgressTree{ mon: monitor, countBySeverity: make(map[vulnerability.Severity]int64), severities: allSeverities, textStyle: textStyle, } } // vulnerabilityProgressTreeTickMsg indicates that the timer has ticked and we should render a frame. type vulnerabilityProgressTreeTickMsg struct { Time time.Time Sequence int ID uint32 } type vulnerabilityScanningAdapter struct { mon *monitor.Matching } func (p vulnerabilityScanningAdapter) Current() int64 { return p.mon.PackagesProcessed.Current() } func (p vulnerabilityScanningAdapter) Error() error { return p.mon.MatchesDiscovered.Error() } func (p vulnerabilityScanningAdapter) Size() int64 { return p.mon.PackagesProcessed.Size() } func (p vulnerabilityScanningAdapter) Stage() string { return fmt.Sprintf("%d vulnerability matches", p.mon.MatchesDiscovered.Current()-p.mon.Ignored.Current()) } func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) ([]tea.Model, tea.Cmd) { mon, err := parsers.ParseVulnerabilityScanningStarted(e) if err != nil { log.WithFields("error", err).Warn("unable to parse event") return nil, nil } tsk := m.newTaskProgress( taskprogress.Title{ Default: "Scan for vulnerabilities", Running: "Scanning for vulnerabilities", Success: "Scanned for vulnerabilities", }, taskprogress.WithStagedProgressable(vulnerabilityScanningAdapter{mon: mon}), ) tsk.HideStageOnSuccess = false textStyle := tsk.HintStyle return []tea.Model{ tsk, newVulnerabilityProgressTree(mon, textStyle), }, nil } func (l vulnerabilityProgressTree) Init() tea.Cmd { // this is the periodic update of state information return func() tea.Msg { return vulnerabilityProgressTreeTickMsg{ // The time at which the tick occurred. Time: time.Now(), // The ID of the log frame that this message belongs to. This can be // helpful when routing messages, however bear in mind that log frames // will ignore messages that don't contain ID by default. ID: l.id, Sequence: l.sequence, } } } func (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: l.windowSize = msg return l, nil case vulnerabilityProgressTreeTickMsg: // update the model l.totalCount = l.mon.MatchesDiscovered.Current() l.fixedCount = l.mon.Fixed.Current() l.ignoredCount = l.mon.Ignored.Current() l.droppedCount = l.mon.Dropped.Current() l.unknownCount = l.mon.BySeverity[vulnerability.UnknownSeverity].Current() for _, sev := range l.severities { l.countBySeverity[sev] = l.mon.BySeverity[sev].Current() } // kick off the next tick tickCmd := l.handleTick(msg) return l, tickCmd } return l, nil } func (l vulnerabilityProgressTree) View() string { sb := strings.Builder{} for idx, sev := range l.severities { count := l.countBySeverity[sev] fmt.Fprintf(&sb, "%d %s", count, sev) if idx < len(l.severities)-1 { sb.WriteString(", ") } } if l.unknownCount > 0 { unknownStr := fmt.Sprintf(" (%d unknown)", l.unknownCount) sb.WriteString(unknownStr) } status := sb.String() sb.Reset() sevStr := l.textStyle.Render(fmt.Sprintf(" %s by severity: %s", branch, status)) sb.WriteString(sevStr) dropped := "" if l.droppedCount > 0 { dropped = fmt.Sprintf("(%d dropped)", l.droppedCount) } fixedStr := l.textStyle.Render( fmt.Sprintf(" %s by status: %d fixed, %d not-fixed, %d ignored %s", end, l.fixedCount, l.totalCount-l.fixedCount, l.ignoredCount, dropped, ), ) sb.WriteString("\n" + fixedStr) sb.WriteString("\n") return sb.String() } func (l vulnerabilityProgressTree) queueNextTick() tea.Cmd { return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg { return vulnerabilityProgressTreeTickMsg{ Time: t, ID: l.id, Sequence: l.sequence, } }) } func (l *vulnerabilityProgressTree) handleTick(msg vulnerabilityProgressTreeTickMsg) tea.Cmd { // If an ID is set, and the ID doesn't belong to this log frame, reject the message. if msg.ID > 0 && msg.ID != l.id { return nil } // If a sequence is set, and it's not the one we expect, reject the message. // This prevents the log frame from receiving too many messages and // thus updating too frequently. if msg.Sequence > 0 && msg.Sequence != l.sequence { return nil } l.sequence++ // note: even if the log is completed we should still respond to stage changes and window size events return l.queueNextTick() } ================================================ FILE: cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go ================================================ package ui import ( "sort" "testing" "time" tea "github.com/charmbracelet/bubbletea" "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/vulnerability" ) func TestHandler_handleVulnerabilityScanningStarted(t *testing.T) { tests := []struct { name string eventFn func(*testing.T) partybus.Event iterations int }{ { name: "vulnerability scanning in progress", eventFn: func(t *testing.T) partybus.Event { return partybus.Event{ Type: event.VulnerabilityScanningStarted, Value: getVulnerabilityMonitor(false), } }, }, { name: "vulnerability scanning complete", eventFn: func(t *testing.T) partybus.Event { return partybus.Event{ Type: event.VulnerabilityScanningStarted, Value: getVulnerabilityMonitor(true), } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := tt.eventFn(t) handler := New(DefaultHandlerConfig()) handler.WindowSize = tea.WindowSizeMsg{ Width: 100, Height: 80, } models, _ := handler.Handle(e) require.Len(t, models, 2) t.Run("task line", func(t *testing.T) { tsk, ok := models[0].(taskprogress.Model) require.True(t, ok) got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ Time: time.Now(), Sequence: tsk.Sequence(), ID: tsk.ID(), }) t.Log(got) snaps.MatchSnapshot(t, got) }) t.Run("tree", func(t *testing.T) { log, ok := models[1].(vulnerabilityProgressTree) require.True(t, ok) got := runModel(t, log, tt.iterations, vulnerabilityProgressTreeTickMsg{ Time: time.Now(), Sequence: log.sequence, ID: log.id, }) t.Log(got) snaps.MatchSnapshot(t, got) }) }) } } func getVulnerabilityMonitor(completed bool) monitor.Matching { pkgs := &progress.Manual{} pkgs.SetTotal(-1) if completed { pkgs.Set(2000) pkgs.SetCompleted() } else { pkgs.Set(300) } vulns := &progress.Manual{} vulns.SetTotal(-1) if completed { vulns.Set(45) vulns.SetCompleted() } else { vulns.Set(40) } fixed := &progress.Manual{} fixed.SetTotal(-1) if completed { fixed.Set(35) fixed.SetCompleted() } else { fixed.Set(30) } ignored := &progress.Manual{} ignored.SetTotal(-1) if completed { ignored.Set(5) ignored.SetCompleted() } else { ignored.Set(4) } dropped := &progress.Manual{} dropped.SetTotal(-1) if completed { dropped.Set(3) dropped.SetCompleted() } else { dropped.Set(2) } bySeverityWriter := map[vulnerability.Severity]*progress.Manual{ vulnerability.CriticalSeverity: {}, vulnerability.HighSeverity: {}, vulnerability.MediumSeverity: {}, vulnerability.LowSeverity: {}, vulnerability.NegligibleSeverity: {}, vulnerability.UnknownSeverity: {}, } allSeverities := vulnerability.AllSeverities() sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) var count int64 = 1 for _, sev := range allSeverities { bySeverityWriter[sev].Add(count) count++ } bySeverityWriter[vulnerability.UnknownSeverity].Add(count) bySeverity := map[vulnerability.Severity]progress.Monitorable{} for k, v := range bySeverityWriter { bySeverity[k] = v } return monitor.Matching{ PackagesProcessed: pkgs, MatchesDiscovered: vulns, Fixed: fixed, Ignored: ignored, Dropped: dropped, BySeverity: bySeverity, } } ================================================ FILE: cmd/grype/cli/ui/handler.go ================================================ package ui import ( "sync" tea "github.com/charmbracelet/bubbletea" "github.com/wagoodman/go-partybus" "github.com/anchore/bubbly" "github.com/anchore/bubbly/bubbles/taskprogress" "github.com/anchore/grype/grype/event" ) var _ interface { bubbly.EventHandler bubbly.MessageListener bubbly.HandleWaiter } = (*Handler)(nil) type HandlerConfig struct { TitleWidth int AdjustDefaultTask func(taskprogress.Model) taskprogress.Model } type Handler struct { WindowSize tea.WindowSizeMsg Running *sync.WaitGroup Config HandlerConfig bubbly.EventHandler } func DefaultHandlerConfig() HandlerConfig { return HandlerConfig{ TitleWidth: 30, } } func New(cfg HandlerConfig) *Handler { d := bubbly.NewEventDispatcher() h := &Handler{ EventHandler: d, Running: &sync.WaitGroup{}, Config: cfg, } // register all supported event types with the respective handler functions d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{ event.UpdateVulnerabilityDatabase: h.handleUpdateVulnerabilityDatabase, event.VulnerabilityScanningStarted: h.handleVulnerabilityScanningStarted, event.DatabaseDiffingStarted: h.handleDatabaseDiffStarted, }) return h } func (m *Handler) OnMessage(msg tea.Msg) { if msg, ok := msg.(tea.WindowSizeMsg); ok { m.WindowSize = msg } } func (m *Handler) Wait() { m.Running.Wait() } ================================================ FILE: cmd/grype/cli/ui/new_task_progress.go ================================================ package ui import "github.com/anchore/bubbly/bubbles/taskprogress" func (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model { tsk := taskprogress.New(m.Running, opts...) tsk.HideProgressOnSuccess = true tsk.HideStageOnSuccess = true tsk.WindowSize = m.WindowSize tsk.TitleWidth = m.Config.TitleWidth tsk.TitleOptions = title if m.Config.AdjustDefaultTask != nil { tsk = m.Config.AdjustDefaultTask(tsk) } return tsk } ================================================ FILE: cmd/grype/cli/ui/util_test.go ================================================ package ui import ( "reflect" "sync" "testing" "unsafe" tea "github.com/charmbracelet/bubbletea" ) func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, wgs ...*sync.WaitGroup) string { t.Helper() if iterations == 0 { iterations = 1 } m.Init() var cmd tea.Cmd = func() tea.Msg { return message } for _, wg := range wgs { if wg != nil { wg.Wait() } } for i := 0; cmd != nil && i < iterations; i++ { msgs := flatten(cmd()) var nextCmds []tea.Cmd var next tea.Cmd for _, msg := range msgs { t.Logf("Message: %+v %+v\n", reflect.TypeOf(msg), msg) m, next = m.Update(msg) nextCmds = append(nextCmds, next) } cmd = tea.Batch(nextCmds...) } return m.View() } func flatten(p tea.Msg) (msgs []tea.Msg) { if reflect.TypeOf(p).Name() == "batchMsg" { partials := extractBatchMessages(p) for _, m := range partials { msgs = append(msgs, flatten(m)...) } } else { msgs = []tea.Msg{p} } return msgs } func extractBatchMessages(m tea.Msg) (ret []tea.Msg) { sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil))) value := reflect.ValueOf(m) // note: this is technically unaddressable // make our own instance that is addressable valueCopy := reflect.New(value.Type()).Elem() valueCopy.Set(value) cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem() for i := 0; i < cmds.Len(); i++ { item := cmds.Index(i) r := item.Call(nil) ret = append(ret, r[0].Interface().(tea.Msg)) } return ret } ================================================ FILE: cmd/grype/internal/constants.go ================================================ package internal const ( NotProvided = "[not provided]" ) ================================================ FILE: cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap ================================================ [Test_postUIEventWriter_write/no_events/stdout - 1] --- [Test_postUIEventWriter_write/no_events/stderr - 1] --- [Test_postUIEventWriter_write/all_events/stdout - 1] --- [Test_postUIEventWriter_write/all_events/stderr - 1] A newer version of grype is available for download: v0.33.0 (installed version is [not provided]) --- [Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1] --- [Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1] --- ================================================ FILE: cmd/grype/internal/ui/no_ui.go ================================================ package ui import ( "os" "github.com/wagoodman/go-partybus" "github.com/anchore/clio" "github.com/anchore/grype/grype/event" ) var _ clio.UI = (*NoUI)(nil) type NoUI struct { finalizeEvents []partybus.Event subscription partybus.Unsubscribable quiet bool } func None(quiet bool) *NoUI { return &NoUI{ quiet: quiet, } } func (n *NoUI) Setup(subscription partybus.Unsubscribable) error { n.subscription = subscription return nil } func (n *NoUI) Handle(e partybus.Event) error { switch e.Type { case event.CLIReport, event.CLINotification: // keep these for when the UI is terminated to show to the screen (or perform other events) n.finalizeEvents = append(n.finalizeEvents, e) } return nil } func (n NoUI) Teardown(_ bool) error { return newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...) } ================================================ FILE: cmd/grype/internal/ui/post_ui_event_writer.go ================================================ package ui import ( "fmt" "io" "strings" "github.com/charmbracelet/lipgloss" "github.com/hashicorp/go-multierror" "github.com/wagoodman/go-partybus" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/parsers" "github.com/anchore/grype/internal/log" ) type postUIEventWriter struct { handles []postUIHandle } type postUIHandle struct { respectQuiet bool event partybus.EventType writer io.Writer dispatch eventWriter } type eventWriter func(io.Writer, ...partybus.Event) error func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter { return &postUIEventWriter{ handles: []postUIHandle{ { event: event.CLIReport, respectQuiet: false, writer: stdout, dispatch: writeReports, }, { event: event.CLINotification, respectQuiet: true, writer: stderr, dispatch: writeNotifications, }, { event: event.CLIAppUpdateAvailable, respectQuiet: true, writer: stderr, dispatch: writeAppUpdate, }, }, } } func (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error { var errs error for _, h := range w.handles { if quiet && h.respectQuiet { continue } for _, e := range events { if e.Type != h.event { continue } if err := h.dispatch(h.writer, e); err != nil { errs = multierror.Append(errs, err) } } } return errs } func writeReports(writer io.Writer, events ...partybus.Event) error { var reports []string for _, e := range events { _, report, err := parsers.ParseCLIReport(e) if err != nil { log.WithFields("error", err).Warn("failed to gather final report") continue } // remove all whitespace padding from the end of the report reports = append(reports, strings.TrimRight(report, "\n ")+"\n") } // prevent the double new-line at the end of the report report := strings.Join(reports, "\n") if _, err := fmt.Fprint(writer, report); err != nil { return fmt.Errorf("failed to write final report to stdout: %w", err) } return nil } func writeNotifications(writer io.Writer, events ...partybus.Event) error { // 13 = high intensity magenta (ANSI 16 bit code) style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")) for _, e := range events { _, notification, err := parsers.ParseCLINotification(e) if err != nil { log.WithFields("error", err).Warn("failed to parse notification") continue } if _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil { // don't let this be fatal log.WithFields("error", err).Warn("failed to write final notifications") } } return nil } func writeAppUpdate(writer io.Writer, events ...partybus.Event) error { // 13 = high intensity magenta (ANSI 16 bit code) + italics style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true) for _, e := range events { version, err := parsers.ParseCLIAppUpdateAvailable(e) if err != nil { log.WithFields("error", err).Warn("failed to parse app update notification") continue } if version.New == "" { continue } notice := fmt.Sprintf("A newer version of grype is available for download: %s (installed version is %s)", version.New, version.Current) if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil { // don't let this be fatal log.WithFields("error", err).Warn("failed to write app update notification") } } return nil } ================================================ FILE: cmd/grype/internal/ui/post_ui_event_writer_test.go ================================================ package ui import ( "bytes" "testing" "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" "github.com/wagoodman/go-partybus" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/parsers" ) func Test_postUIEventWriter_write(t *testing.T) { tests := []struct { name string quiet bool events []partybus.Event wantErr require.ErrorAssertionFunc }{ { name: "no events", }, { name: "all events", events: []partybus.Event{ { Type: event.CLINotification, Value: "\n\n\n\n", }, { Type: event.CLINotification, Value: "", }, { Type: event.CLIAppUpdateAvailable, Value: parsers.UpdateCheck{ New: "v0.33.0", Current: "[not provided]", }, }, { Type: event.CLINotification, Value: "", }, { Type: event.CLIReport, Value: "\n\n\n\n", }, { Type: event.CLIReport, Value: "", }, }, }, { name: "quiet only shows report", quiet: true, events: []partybus.Event{ { Type: event.CLINotification, Value: "", }, { Type: event.CLIAppUpdateAvailable, Value: parsers.UpdateCheck{ New: "", Current: "", }, }, { Type: event.CLIReport, Value: "", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} w := newPostUIEventWriter(stdout, stderr) tt.wantErr(t, w.write(tt.quiet, tt.events...)) t.Run("stdout", func(t *testing.T) { snaps.MatchSnapshot(t, stdout.String()) }) t.Run("stderr", func(t *testing.T) { snaps.MatchSnapshot(t, stderr.String()) }) }) } } ================================================ FILE: cmd/grype/internal/ui/ui.go ================================================ package ui import ( "fmt" "os" "sync" "time" tea "github.com/charmbracelet/bubbletea" "github.com/wagoodman/go-partybus" "github.com/anchore/bubbly" "github.com/anchore/bubbly/bubbles/frame" "github.com/anchore/clio" "github.com/anchore/go-logger" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" ) var _ interface { tea.Model partybus.Responder clio.UI } = (*UI)(nil) type UI struct { program *tea.Program running *sync.WaitGroup quiet bool subscription partybus.Unsubscribable finalizeEvents []partybus.Event handler *bubbly.HandlerCollection frame tea.Model } func New(quiet bool, handlers ...bubbly.EventHandler) *UI { return &UI{ handler: bubbly.NewHandlerCollection(handlers...), frame: frame.New(), running: &sync.WaitGroup{}, quiet: quiet, } } func (m *UI) Setup(subscription partybus.Unsubscribable) error { // we still want to collect log messages, however, we also the logger shouldn't write to the screen directly if logWrapper, ok := log.Get().(logger.Controller); ok { logWrapper.SetOutput(m.frame.(*frame.Frame).Footer()) } m.subscription = subscription m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin)) m.running.Add(1) go func() { defer m.running.Done() if _, err := m.program.Run(); err != nil { log.Errorf("unable to start UI: %+v", err) bus.ExitWithInterrupt() } }() return nil } func (m *UI) Handle(e partybus.Event) error { if m.program != nil { m.program.Send(e) } return nil } func (m *UI) Teardown(force bool) error { defer func() { // allow for traditional logging to resume now that the UI is shutting down if logWrapper, ok := log.Get().(logger.Controller); ok { logWrapper.SetOutput(os.Stderr) } }() if !force { m.handler.Wait() m.program.Quit() // typically in all cases we would want to wait for the UI to finish. However there are still error cases // that are not accounted for, resulting in hangs. For now, we'll just wait for the UI to finish in the // happy path only. There will always be an indication of the problem to the user via reporting the error // string from the worker (outside of the UI after teardown). m.running.Wait() } else { _ = runWithTimeout(250*time.Millisecond, func() error { m.handler.Wait() return nil }) // it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in // a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal). m.program.Quit() _ = runWithTimeout(250*time.Millisecond, func() error { m.running.Wait() return nil }) } // TODO: allow for writing out the full log output to the screen (only a partial log is shown currently) // this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now) return newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...) } // bubbletea.Model functions func (m UI) Init() tea.Cmd { return m.frame.Init() } func (m UI) RespondsTo() []partybus.EventType { return append([]partybus.EventType{ event.CLIReport, event.CLINotification, event.CLIAppUpdateAvailable, }, m.handler.RespondsTo()...) } func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events) var cmds []tea.Cmd // allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models, // that is the responsibility of the frame object on this UI object. The handler is a factory of models // which the frame is responsible for the lifecycle of. This update allows for injecting the initial state // of the world when creating those models. m.handler.OnMessage(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { // today we treat esc and ctrl+c the same, but in the future when the worker has a graceful way to // cancel in-flight work via a context, we can wire up esc to this path with bus.Exit() case "esc", "ctrl+c": bus.ExitWithInterrupt() return m, tea.Quit } case partybus.Event: log.WithFields("component", "ui").Tracef("event: %q", msg.Type) switch msg.Type { case event.CLIReport, event.CLINotification, event.CLIAppUpdateAvailable: // keep these for when the UI is terminated to show to the screen (or perform other events) m.finalizeEvents = append(m.finalizeEvents, msg) // why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop. // for this reason we'll let the event loop call Teardown() which will explicitly wait for these components return m, nil } models, cmd := m.handler.Handle(msg) if cmd != nil { cmds = append(cmds, cmd) } for _, newModel := range models { if newModel == nil { continue } cmds = append(cmds, newModel.Init()) m.frame.(*frame.Frame).AppendModel(newModel) } // intentionally fallthrough to update the frame model } frameModel, cmd := m.frame.Update(msg) m.frame = frameModel cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m UI) View() string { return m.frame.View() } func runWithTimeout(timeout time.Duration, fn func() error) (err error) { c := make(chan struct{}, 1) go func() { err = fn() c <- struct{}{} }() select { case <-c: case <-time.After(timeout): return fmt.Errorf("timed out after %v", timeout) } return err } ================================================ FILE: cmd/grype/main.go ================================================ package main import ( _ "github.com/glebarez/sqlite" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli" "github.com/anchore/grype/cmd/grype/internal" ) // applicationName is the non-capitalized name of the application (do not change this) const applicationName = "grype" // all variables here are provided as build-time arguments, with clear default values var ( version = internal.NotProvided buildDate = internal.NotProvided gitCommit = internal.NotProvided gitDescription = internal.NotProvided ) func main() { app := cli.Application( clio.Identification{ Name: applicationName, Version: version, BuildDate: buildDate, GitCommit: gitCommit, GitDescription: gitDescription, }, ) app.Run() } ================================================ FILE: go.mod ================================================ module github.com/anchore/grype go 1.25.8 require ( github.com/CycloneDX/cyclonedx-go v0.10.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/OneOfOne/xxhash v1.2.8 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.5.3 github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 github.com/anchore/stereoscope v0.1.22 github.com/anchore/syft v1.42.3 github.com/aquasecurity/go-pep440-version v0.0.1 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607 github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/dave/jennifer v1.7.1 github.com/docker/docker v28.5.2+incompatible github.com/dustin/go-humanize v1.0.1 github.com/facebookincubator/nvdtools v0.1.5 github.com/gabriel-vasile/mimetype v1.4.13 github.com/gkampitakis/go-snaps v0.5.20 github.com/glebarez/sqlite v1.11.0 github.com/go-test/deep v1.1.1 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/gocsaf/csaf/v3 v3.5.1 github.com/gohugoio/hashstructure v0.6.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.21.2 github.com/google/osv-scanner v1.9.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gookit/color v1.6.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-getter v1.8.5 github.com/hashicorp/go-multierror v1.1.1 github.com/iancoleman/strcase v0.3.0 github.com/invopop/jsonschema v0.13.0 github.com/jinzhu/copier v0.4.0 github.com/klauspost/compress v1.18.4 github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a github.com/mholt/archives v0.1.5 github.com/muesli/termenv v0.16.0 github.com/olekukonko/tablewriter v1.1.4 github.com/openvex/go-vex v0.2.7 github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 github.com/pandatix/go-cvss v0.6.2 // pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e github.com/sergi/go-diff v1.4.0 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/ulikunitz/xz v0.5.15 github.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23 github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b github.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 golang.org/x/text v0.35.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.43.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.31.1 ) require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect cloud.google.com/go/storage v1.60.0 // indirect cyphar.com/go-pathrs v0.2.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/DataDog/zstd v1.5.7 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/Intevation/gval v1.3.0 // indirect github.com/Intevation/jsonpath v0.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/STARRY-S/zip v0.2.3 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/anchore/go-lzo v0.1.0 // indirect github.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 // indirect github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec // indirect github.com/anchore/go-struct-converter v0.1.0 // indirect github.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/smithy-go v1.24.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/bubbles v1.0.0 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/cgroups/v3 v3.1.2 // indirect github.com/containerd/containerd/api v1.10.0 // indirect github.com/containerd/containerd/v2 v2.2.1 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v1.0.0-rc.2 // indirect github.com/containerd/plugin v1.0.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb // indirect github.com/diskfs/go-diskfs v1.7.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.3.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/elliotchance/phpserialize v1.4.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/github/go-spdx/v2 v2.4.0 // indirect github.com/gkampitakis/ciinfo v0.3.2 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-git/go-git/v5 v5.17.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gpustack/gguf-parser-go v0.24.0 // indirect github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/henvic/httpretty v0.1.4 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/maruel/natural v1.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/moby/api v1.54.0 // indirect github.com/moby/moby/client v0.3.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/ll v0.1.6 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/opencontainers/selinux v1.13.1 // indirect github.com/package-url/packageurl-go v0.1.3 // indirect github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.7.0 // indirect github.com/pkg/xattr v0.4.12 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sassoftware/go-rpmutils v0.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb // indirect github.com/spdx/tools-golang v0.5.7 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/sif/v2 v2.24.0 // indirect github.com/sylabs/squashfs v1.0.6 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vbatts/go-mtree v0.7.0 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/vifraa/gopom v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zclconf/go-cty v1.16.3 // indirect github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 // indirect go.etcd.io/bbolt v1.4.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/api v0.267.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.46.1 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8= cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y= github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw= github.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o= github.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A= github.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf h1:UY7SQkfVVaeGUpPZrJxqmTc8M0ZSWc5ChiKF6I6aL3I= github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf/go.mod h1:w8Br1ZKk1Nk82YRSh10pcD7LO7avPyFmNnaY1TRPgs0= github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 h1:7DUAXEdAxoANPlDgxYiaSRKnWnTygvdrrWhnmvEjNLg= github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084/go.mod h1:42dWox8z4//b898OIELsQnSdYq9q1aCXkwp5fKF+BEU= github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 h1:aVC6r9h5wGNh8BYTW3CXxOdPoZzY/bBRWne1NvSTlO8= github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232/go.mod h1:Zees1AEKNpXIRgdVAMYWITncarLFiPOtEQ7rl45V/h0= github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c h1:eoJXyC0n7DZ4YvySG/ETdYkTar2Due7eH+UmLK6FbrA= github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 h1:fWPHXkH3FQGVCyPkFMqNvMjQvdNMfkylBTsDqZC4lE4= github.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331/go.mod h1:DYvTRnWrlJ//6YOR83SiewmJiNFDEMRaOTnrzgco9FA= github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg= github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU= github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1 h1:UK1SWZf2xD5jq8QVeDdpt6wW31cO3RckBvPmGlDrTkg= github.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1/go.mod h1:hd0Ol9qFM8tRDdF50a+DpZEoB0HFNaEnCp/BSVyBRlg= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= github.com/anchore/stereoscope v0.1.22 h1:L807G/kk0WZzOCGuRGF7knxMKzwW2PGdbPVRystryd8= github.com/anchore/stereoscope v0.1.22/go.mod h1:FikPtAb/WnbqwgLHAvQA9O+fWez0K4pbjxzghz++iy4= github.com/anchore/syft v1.42.3 h1:eIeeGyqfXm/C8wpBWU50xFbOjdL37VbLatMj9nEJ6n4= github.com/anchore/syft v1.42.3/go.mod h1:i2PZ+276IdPcnd/n32aeIv849iO/QqdjRknbIc39yL0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M= github.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4= github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E= github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607 h1:lBg3tHGquFySSblLi9zNi2iGNmVLRHBzVal2fqphCM8= github.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk= github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb h1:4W/2rQ3wzEimF5s+J6OY3ODiQtJZ5W1sForSgogVXkY= github.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8= github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY= github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ= github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/github/go-spdx/v2 v2.4.0 h1:+4IwVwJJbm3rzvrQ6P1nI9BDMcy3la4RchRy5uehV/M= github.com/github/go-spdx/v2 v2.4.0/go.mod h1:/5rwgS0txhGtRdUZwc02bTglzg6HK3FfuEbECKlK2Sg= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-snaps v0.5.20 h1:FGKonEeQPJ12t7RQj6cTPa881fl5c8HYarMLv5vP7sg= github.com/gkampitakis/go-snaps v0.5.20/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gocsaf/csaf/v3 v3.5.1 h1:jTA1fLrK0/JIczPs7itTD53qANoO4tn2VaGvUeitePc= github.com/gocsaf/csaf/v3 v3.5.1/go.mod h1:pga89lE+iWJm7smTdzYcXuetYUbgY8caXfaIP4BJG98= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg= github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0= github.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/osv-scanner v1.9.2 h1:N5Arl9SA75afbjmX8mKURgOIaKyuK3NUjCaxDlj1KHI= github.com/google/osv-scanner v1.9.2/go.mod h1:ZTL8Dp9z/7Jr9kkQSOGqo8z6Csqt83qMIr58aZVx+pM= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gpustack/gguf-parser-go v0.24.0 h1:tdJceXYp9e5RhE9RwVYIuUpir72Jz2D68NEtDXkKCKc= github.com/gpustack/gguf-parser-go v0.24.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-getter v1.8.5 h1:DMPV5CSw5JrNg/IK7kDZt3+l2REKXOi3oAw7uYLh2NM= github.com/hashicorp/go-getter v1.8.5/go.mod h1:WIffejwAyDSJhoVptc3UEshEMkR9O63rw34V7k43O3Q= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y= github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg= github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s= github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s= github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a/go.mod h1:jZ3F25l7DbD7l7DcA8aj7eo1EZ84nbzcQHBB4lCSrI8= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 h1:kpt9ZfKcm+EDG4s40hMwE//d5SBgDjUOrITReV2u4aA= github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1/go.mod h1:qgCw4bBKZX8qMgGeEZzGFVT3notl42dBjNqO2jut0M0= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/openvex/go-vex v0.2.7 h1:/pN3bqvS4QOc6WkkL0hbKzJuAtsUD9vmvk9IZkzD3Zc= github.com/openvex/go-vex v0.2.7/go.mod h1:ZyQC3NXl9jjS53JOpBG3LAUXySkW8IlJ/GIhsnf5D54= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKpPqik5WsQ8+4z4DKWgA1tO1RTTtNKr5oYNA= github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94= github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI= github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0= github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d h1:3VwvTjiRPA7cqtgOWddEL+JrcijMlXUmj99c/6YyZoY= github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d/go.mod h1:tAG61zBM1DYRaGIPloumExGvScf08oHuo0kFoOqdbT0= github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb h1:7G2Czq97VORM5xNRrD8tSQdhoXPRs8s+Otlc7st9TS0= github.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w= github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88= github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23 h1:+168JmE638t0OxroPRx7BUbkB91hF3GWS1OkvITgdT0= github.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23/go.mod h1:Jv/KoYWD3+46wW8r3pEwISwtgv5Q8NTfFto2wFRKvoA= github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E= github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0= github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s= github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8= github.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0 h1:EHsPe0Q0ANoLOZff1dBLAyeWLTA4sbPTpGI+2zb0FnM= github.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0/go.mod h1:g/D9uEUFp5YLyciwCpVsSOZOm56hfv4rzGJod6MlqIM= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 h1:V+UsotZpAVvfj3X/LMoEytoLzSiP6Lg0F7wdVyu9gGg= github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: grype/cpe/cpe.go ================================================ package cpe import ( "github.com/facebookincubator/nvdtools/wfn" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) func NewSlice(cpeStrs ...string) ([]cpe.CPE, error) { var cpes []cpe.CPE for _, c := range cpeStrs { value, err := cpe.New(c, "") if err != nil { log.Warnf("excluding invalid CPE %q: %v", c, err) continue } cpes = append(cpes, value) } return cpes, nil } func MatchWithoutVersion(c cpe.CPE, candidates []cpe.CPE) []cpe.CPE { matches := make([]cpe.CPE, 0) a := wfn.Attributes(c.Attributes) a.Update = wfn.Any for _, candidate := range candidates { canCopy := wfn.Attributes(candidate.Attributes) canCopy.Update = wfn.Any if a.MatchWithoutVersion(&canCopy) { matches = append(matches, candidate) } } return matches } ================================================ FILE: grype/cpe/cpe_test.go ================================================ package cpe import ( "testing" "github.com/sergi/go-diff/diffmatchpatch" "github.com/anchore/syft/syft/cpe" ) func TestMatchWithoutVersion(t *testing.T) { tests := []struct { name string compare cpe.CPE candidates []cpe.CPE expected []cpe.CPE }{ { name: "GoCase", compare: cpe.Must("cpe:2.3:*:python-requests:requests:2.3.0:*:*:*:*:python:*:*", ""), candidates: []cpe.CPE{ cpe.Must("cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*", ""), }, expected: []cpe.CPE{ cpe.Must("cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*", ""), }, }, { name: "IgnoreVersion", compare: cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""), candidates: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*", ""), }, expected: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*", ""), }, }, { name: "MatchByTargetSW", compare: cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""), candidates: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:maven:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:jenkins:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*", ""), }, expected: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*", ""), }, }, { name: "MatchByName", compare: cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:java:*:*", ""), candidates: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name1:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name2:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name3:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name4:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*", ""), }, expected: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*", ""), }, }, { name: "MatchByVendor", compare: cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""), candidates: []cpe.CPE{ cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*", ""), cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*", ""), }, expected: []cpe.CPE{ cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""), }, }, { name: "MatchAnyVendorOrTargetSW", compare: cpe.Must("cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*", ""), candidates: []cpe.CPE{ cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*", ""), cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:*:name5:NOMATCH:3.2:*:*:*:*:*:*:*", ""), }, expected: []cpe.CPE{ cpe.Must("cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*", ""), cpe.Must("cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*", ""), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := MatchWithoutVersion(test.compare, test.candidates) if len(actual) != len(test.expected) { for _, e := range actual { t.Errorf(" unexpected entry: %+v", e.Attributes.BindToFmtString()) } t.Fatalf("unexpected number of entries: %d", len(actual)) } for idx, a := range actual { e := test.expected[idx] if a.Attributes.BindToFmtString() != e.Attributes.BindToFmtString() { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(a.Attributes.BindToFmtString(), e.Attributes.BindToFmtString(), true) t.Errorf("mismatched entries @ %d:\n\texpected:%+v\n\t actual:%+v\n\t diff:%+v\n", idx, e.Attributes.BindToFmtString(), a.Attributes.BindToFmtString(), dmp.DiffPrettyText(diffs)) } } }) } } ================================================ FILE: grype/db/build.go ================================================ package db import ( "bytes" "fmt" "sort" "time" "github.com/dustin/go-humanize" "github.com/spf13/afero" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/grype/db/provider/entry" grypeDBv5 "github.com/anchore/grype/grype/db/v5" v5 "github.com/anchore/grype/grype/db/v5/build" grypeDBv6 "github.com/anchore/grype/grype/db/v6" v6 "github.com/anchore/grype/grype/db/v6/build" "github.com/anchore/grype/internal/log" ) // DefaultBatchSize is the default number of database operations to batch together // before flushing to disk. This value balances throughput with memory usage. const DefaultBatchSize = 2000 type BuildConfig struct { SchemaVersion int Directory string States provider.States Timestamp time.Time IncludeCPEParts []string InferNVDFixVersions bool Hydrate bool FailOnMissingFixDate bool // any fixes found without at least one available date will cause a build failure BatchSize int // number of operations to batch before committing } func Build(cfg BuildConfig) error { log.WithFields( "schema", cfg.SchemaVersion, "build-directory", cfg.Directory, "providers", cfg.States.Names()). Info("building database") processors, err := getProcessors(cfg) if err != nil { return err } writer, err := getWriter(cfg) if err != nil { return err } var openers []providerResults for _, sd := range cfg.States { sdOpeners, count, err := entry.Openers(sd.Store, sd.ResultPaths()) if err != nil { return fmt.Errorf("failed to open provider result files: %w", err) } openers = append(openers, providerResults{ openers: sdOpeners, provider: sd, count: count, }) } if err := build(openers, writer, processors...); err != nil { return err } if err := writer.Close(); err != nil { return err } if cfg.Hydrate && cfg.SchemaVersion > 5 { if err := hydrate(cfg); err != nil { return err } } return nil } type providerResults struct { openers <-chan entry.Opener provider provider.State count int64 } func getProcessors(cfg BuildConfig) ([]data.Processor, error) { switch cfg.SchemaVersion { case grypeDBv5.SchemaVersion: return v5.Processors(v5.NewConfig(v5.WithCPEParts(cfg.IncludeCPEParts), v5.WithInferNVDFixVersions(cfg.InferNVDFixVersions))), nil case grypeDBv6.ModelVersion: return v6.Processors(v6.NewConfig(v6.WithCPEParts(cfg.IncludeCPEParts), v6.WithInferNVDFixVersions(cfg.InferNVDFixVersions))), nil default: return nil, fmt.Errorf("unable to create processor: unsupported schema version: %+v", cfg.SchemaVersion) } } func getWriter(cfg BuildConfig) (data.Writer, error) { // Use default if not configured batchSize := cfg.BatchSize if batchSize == 0 { batchSize = DefaultBatchSize } switch cfg.SchemaVersion { case grypeDBv5.SchemaVersion: return v5.NewWriter(cfg.Directory, cfg.Timestamp, cfg.States, batchSize) case grypeDBv6.ModelVersion: return v6.NewWriter(cfg.Directory, cfg.States, cfg.FailOnMissingFixDate, batchSize) default: return nil, fmt.Errorf("unable to create writer: unsupported schema version: %+v", cfg.SchemaVersion) } } func build(results []providerResults, writer data.Writer, processors ...data.Processor) error { // nolint:funlen lastUpdate := time.Now() var totalRecords int for _, result := range results { totalRecords += int(result.count) } log.WithFields("total", humanize.Comma(int64(totalRecords))).Info("processing all records") // for exponential moving average, choose an alpha between 0 and 1, where 1 biases towards the most recent sample // and 0 biases towards the average of all samples. rateWindow := newEMA(0.4) var recordsProcessed, recordsObserved, dropped int droppedElementsByProvider := make(map[string]int) droppedSchemaElements := make(map[string]int) for _, result := range results { log.WithFields("provider", result.provider.Provider, "total", humanize.Comma(result.count)).Info("processing provider records") providerRecordsObserved := 0 recordsObservedInStatusCycle := 0 for opener := range result.openers { providerRecordsObserved++ recordsObserved++ recordsObservedInStatusCycle++ var processor data.Processor if time.Since(lastUpdate) > 3*time.Second { r := recordsPerSecond(recordsObservedInStatusCycle, lastUpdate) rateWindow.Add(r) log.WithFields( "provider", fmt.Sprintf("%q %1.0f/s (%1.2f%%)", result.provider.Provider, r, percent(providerRecordsObserved, int(result.count))), "overall", fmt.Sprintf("%1.2f%%", percent(recordsObserved, totalRecords)), "eta", eta(recordsObserved, totalRecords, rateWindow.Average()).String(), ).Debug("status") lastUpdate = time.Now() recordsObservedInStatusCycle = 0 } f, err := opener.Open() if err != nil { return fmt.Errorf("failed to open cache entry %q: %w", opener.String(), err) } envelope, err := unmarshal.Envelope(f) if err != nil { return fmt.Errorf("failed to unmarshal cache entry %q: %w", opener.String(), err) } for _, candidate := range processors { if candidate.IsSupported(envelope.Schema) { processor = candidate break } } if processor == nil { droppedElementsByProvider[result.provider.Provider]++ droppedSchemaElements[envelope.Schema]++ dropped++ continue } recordsProcessed++ entries, err := processor.Process(bytes.NewReader(envelope.Item), result.provider) if err != nil { return fmt.Errorf("failed to process cache entry %q: %w", opener.String(), err) } if err := writer.Write(entries...); err != nil { return fmt.Errorf("failed to write records to the DB for cache entry %q: %w", opener.String(), err) } } } logDropped(droppedElementsByProvider, droppedSchemaElements) log.WithFields("processed", recordsProcessed, "dropped", dropped, "observed", recordsObserved).Debugf("wrote all provider state") if recordsProcessed == 0 { return fmt.Errorf("no records were processed") } return nil } func hydrate(cfg BuildConfig) error { hydrator := grypeDBv6.Hydrater() fs := afero.NewOsFs() if err := hydrator(cfg.Directory); err != nil { return fmt.Errorf("failed to hydrate db: %w", err) } doc, err := grypeDBv6.WriteImportMetadata(fs, cfg.Directory, "grype db build") if err != nil { return fmt.Errorf("failed to write checksums file: %w", err) } log.WithFields("digest", doc.Digest).Trace("captured DB digest") return nil } func logDropped(droppedElementsByProvider, droppedSchemaElements map[string]int) { sortedKeys := func(m map[string]int) []string { var keys []string for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys } sortedProviders := sortedKeys(droppedElementsByProvider) for _, p := range sortedProviders { log.WithFields("provider", p, "count", droppedElementsByProvider[p]).Warn("dropped records for provider") } sortedSchemas := sortedKeys(droppedSchemaElements) for _, s := range sortedSchemas { log.WithFields("schema", s, "count", droppedSchemaElements[s]).Warn("dropped records by schema") } } type expMovingAverage struct { alpha float64 value float64 count int } func newEMA(alpha float64) *expMovingAverage { return &expMovingAverage{alpha: alpha} } func (e *expMovingAverage) Add(sample float64) { if e.count == 0 { e.value = sample // initialize with the first sample } else { e.value = e.alpha*sample + (1-e.alpha)*e.value } e.count++ } func (e *expMovingAverage) Average() float64 { return e.value } func recordsPerSecond(idx int, lastUpdate time.Time) float64 { sec := time.Since(lastUpdate).Seconds() if sec == 0 { return 0 } return float64(idx) / sec } func percent(idx, total int) float64 { if total == 0 { return 0 } return float64(idx) / float64(total) * 100 } func eta(idx, total int, rate float64) time.Duration { if rate == 0 { return 0 } return time.Duration(float64(total-idx)/rate) * time.Second } ================================================ FILE: grype/db/data/entry.go ================================================ package data // Entry is a data structure responsible for capturing an individual writable entry from a data.Processor (written by a data.Writer). type Entry struct { DBSchemaVersion int // Data is the specific payload that should be written (usually a grype-db v*.Entry struct) Data interface{} } ================================================ FILE: grype/db/data/processor.go ================================================ package data import ( "io" "github.com/anchore/grype/grype/db/provider" ) // Processor takes individual feed group cache files (for select feed groups) and is responsible to producing // data.Entry objects to be written to the DB. type Processor interface { IsSupported(schemaURL string) bool Process(reader io.Reader, state provider.State) ([]Entry, error) } ================================================ FILE: grype/db/data/severity.go ================================================ package data import "strings" type Severity string const ( SeverityUnknown Severity = "Unknown" SeverityNegligible Severity = "Negligible" SeverityLow Severity = "Low" SeverityMedium Severity = "Medium" SeverityHigh Severity = "High" SeverityCritical Severity = "Critical" ) func ParseSeverity(s string) Severity { clean := strings.TrimSpace(strings.ToLower(s)) switch clean { case "unknown", "": return SeverityUnknown case "negligible": return SeverityNegligible case "low": return SeverityLow case "medium": return SeverityMedium case "high": return SeverityHigh case "critical": return SeverityCritical default: return SeverityUnknown } } ================================================ FILE: grype/db/data/severity_test.go ================================================ package data import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseSeverity(t *testing.T) { tests := []struct { input string want Severity }{ { input: "negLIGible", want: SeverityNegligible, }, { input: "loW", want: SeverityLow, }, { input: "meDIum", want: SeverityMedium, }, { input: " hiGH", want: SeverityHigh, }, { input: "cRiTical ", want: SeverityCritical, }, { input: "unKNOWN", want: SeverityUnknown, }, { input: "", want: SeverityUnknown, }, { input: " ", want: SeverityUnknown, }, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { assert.Equal(t, tt.want, ParseSeverity(tt.input)) }) } } ================================================ FILE: grype/db/data/transformers.go ================================================ package data import ( "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" ) // Transformers are functions that know how ta take individual data shapes defined in the unmarshal package and // reshape the data into data.Entry objects that are writable by a data.Writer. Transformers are dependency-injected // into commonly-shared data.Processors in the individual process.v* packages. // all v1 transformers (schema v1 - v5) type GitHubTransformer func(entry unmarshal.GitHubAdvisory) ([]Entry, error) type MSRCTransformer func(entry unmarshal.MSRCVulnerability) ([]Entry, error) type NVDTransformer func(entry unmarshal.NVDVulnerability) ([]Entry, error) type OSTransformer func(entry unmarshal.OSVulnerability) ([]Entry, error) type MatchExclusionTransformer func(entry unmarshal.MatchExclusion) ([]Entry, error) // all v2 transformers (schema v6+) type GitHubTransformerV2 func(entry unmarshal.GitHubAdvisory, state provider.State) ([]Entry, error) type MSRCTransformerV2 func(entry unmarshal.MSRCVulnerability, state provider.State) ([]Entry, error) type NVDTransformerV2 func(entry unmarshal.NVDVulnerability, state provider.State) ([]Entry, error) type OSTransformerV2 func(entry unmarshal.OSVulnerability, state provider.State) ([]Entry, error) type MatchExclusionTransformerV2 func(entry unmarshal.MatchExclusion, state provider.State) ([]Entry, error) type KnownExploitedVulnerabilityTransformerV2 func(entry unmarshal.KnownExploitedVulnerability, state provider.State) ([]Entry, error) type EPSSTransformerV2 func(entry unmarshal.EPSS, state provider.State) ([]Entry, error) type OSVTransformerV2 func(entry unmarshal.OSVVulnerability, state provider.State) ([]Entry, error) type OpenVEXTransformerV2 func(entry unmarshal.OpenVEXVulnerability, state provider.State) ([]Entry, error) type AnnotatedOpenVEXTransformerV2 func(entry unmarshal.AnnotatedOpenVEXVulnerability, state provider.State) ([]Entry, error) type EOLTransformerV2 func(entry unmarshal.EndOfLifeDateRelease, state provider.State) ([]Entry, error) ================================================ FILE: grype/db/data/writer.go ================================================ package data // Writer knows how to persist one or more data.Entry objects to a database. Note that the backing implementations // may take advantage of bulk writes when possible (positively improving performance), which is why multiple // entries can be written at once. type Writer interface { Write(...Entry) error Close() error } ================================================ FILE: grype/db/default_schema_version.go ================================================ package db import db "github.com/anchore/grype/grype/db/v6" const DefaultSchemaVersion = db.ModelVersion ================================================ FILE: grype/db/generate.go ================================================ package db //go:generate go run ./internal/codename/generate/main.go ================================================ FILE: grype/db/internal/codename/codename.go ================================================ package codename import "strings" func LookupOS(osName, majorVersion, minorVersion string) string { majorVersion = strings.TrimLeft(majorVersion, "0") if minorVersion != "0" { minorVersion = strings.TrimLeft(minorVersion, "0") } // try to find the most specific match (major and minor version) if versions, ok := normalizedOSCodenames[osName]; ok { if minorMap, ok := versions[majorVersion]; ok { if codename, ok := minorMap[minorVersion]; ok { return codename } // fall back to the least specific match (only major version, allowing for any minor version explicitly) if codename, ok := minorMap["*"]; ok { return codename } } } return "" } ================================================ FILE: grype/db/internal/codename/codename_test.go ================================================ package codename import ( "testing" "github.com/stretchr/testify/assert" ) func TestLookupOSCodename(t *testing.T) { tests := []struct { Name string OSName string MajorVersion string MinorVersion string ExpectedCodename string }{ {Name: "Ubuntu 20.04 exact", OSName: "ubuntu", MajorVersion: "20", MinorVersion: "04", ExpectedCodename: "focal"}, {Name: "Ubuntu 20.4 exact", OSName: "ubuntu", MajorVersion: "20", MinorVersion: "4", ExpectedCodename: "focal"}, {Name: "Ubuntu 0 (non existent) minor", OSName: "ubuntu", MajorVersion: "20", MinorVersion: "0", ExpectedCodename: ""}, {Name: "Ubuntu empty minor", OSName: "ubuntu", MajorVersion: "10", MinorVersion: "", ExpectedCodename: ""}, {Name: "Debian empty minor", OSName: "debian", MajorVersion: "10", MinorVersion: "", ExpectedCodename: "buster"}, {Name: "Ubuntu leading zeros in major", OSName: "ubuntu", MajorVersion: "020", MinorVersion: "04", ExpectedCodename: "focal"}, {Name: "Debian leading zeros in major", OSName: "debian", MajorVersion: "010", MinorVersion: "", ExpectedCodename: "buster"}, {Name: "Debian bad minor", OSName: "debian", MajorVersion: "11", MinorVersion: "99", ExpectedCodename: "bullseye"}, {Name: "Ubuntu bad minor", OSName: "ubuntu", MajorVersion: "22", MinorVersion: "99", ExpectedCodename: ""}, {Name: "Ubuntu 6.10 exact (legacy)", OSName: "ubuntu", MajorVersion: "6", MinorVersion: "10", ExpectedCodename: "edgy"}, {Name: "Ubuntu 6.6 exact (legacy)", OSName: "ubuntu", MajorVersion: "6", MinorVersion: "6", ExpectedCodename: "dapper"}, {Name: "Debian 2.1 exact", OSName: "debian", MajorVersion: "2", MinorVersion: "1", ExpectedCodename: "slink"}, {Name: "Debian 2 fallback to *", OSName: "debian", MajorVersion: "2", MinorVersion: "0", ExpectedCodename: "hamm"}, {Name: "Invalid OS name", OSName: "nonexistentOS", MajorVersion: "10", MinorVersion: "04", ExpectedCodename: ""}, {Name: "Invalid major version", OSName: "ubuntu", MajorVersion: "99", MinorVersion: "04", ExpectedCodename: ""}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { actualCodename := LookupOS(tt.OSName, tt.MajorVersion, tt.MinorVersion) assert.Equal(t, tt.ExpectedCodename, actualCodename) }) } } ================================================ FILE: grype/db/internal/codename/codenames_generated.go ================================================ // DO NOT EDIT: generated by grype/db/internal/codename/generate/main.go package codename var normalizedOSCodenames = map[string]map[string]map[string]string{ "debian": { "1": { "1": "buzz", "2": "rex", "3": "bo", }, "10": {"*": "buster"}, "11": {"*": "bullseye"}, "12": {"*": "bookworm"}, "13": {"*": "trixie"}, "2": { "0": "hamm", "1": "slink", "2": "potato", }, "3": { "0": "woody", "1": "sarge", }, "4": {"*": "etch"}, "5": {"*": "lenny"}, "6": {"*": "squeeze"}, "7": {"*": "wheezy"}, "8": {"*": "jessie"}, "9": {"*": "stretch"}, }, "ubuntu": { "10": { "10": "maverick", "4": "lucid", }, "11": { "10": "oneiric", "4": "natty", }, "12": { "10": "quantal", "4": "precise", }, "13": { "10": "saucy", "4": "raring", }, "14": { "10": "utopic", "4": "trusty", }, "15": { "10": "wily", "4": "vivid", }, "16": { "10": "yakkety", "4": "xenial", }, "17": { "10": "artful", "4": "zesty", }, "18": { "10": "cosmic", "4": "bionic", }, "19": { "10": "eoan", "4": "disco", }, "20": { "10": "groovy", "4": "focal", }, "21": { "10": "impish", "4": "hirsute", }, "22": { "10": "kinetic", "4": "jammy", }, "23": { "10": "mantic", "4": "lunar", }, "24": { "10": "oracular", "4": "noble", }, "25": { "10": "questing", "4": "plucky", }, "4": {"10": "warty"}, "5": { "10": "breezy", "4": "hoary", }, "6": { "10": "edgy", "6": "dapper", }, "7": { "10": "gutsy", "4": "feisty", }, "8": { "10": "intrepid", "4": "hardy", }, "9": { "10": "karmic", "4": "jaunty", }, }, } ================================================ FILE: grype/db/internal/codename/generate/main.go ================================================ package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "github.com/dave/jennifer/jen" ) const ( outputPackage = "grype/db/internal/codename" outputPath = "internal/codename/codenames_generated.go" // relative to where go generate is called ) type Version struct { Cycle string `json:"cycle"` Codename string `json:"codename"` } func main() { osCodenames := make(map[string]map[string]map[string]string) fmt.Println("Fetching and parsing data for operating system codenames") fmt.Println("ubuntu:") osCodenames["ubuntu"] = fetchAndParse("https://endoflife.date/api/ubuntu.json", ubuntuHandler) fmt.Println("debian:") osCodenames["debian"] = fetchAndParse("https://endoflife.date/api/debian.json", lowercaseHandler) fmt.Printf("Generating code for %d operating system codenames\n", len(osCodenames)) f := jen.NewFile("codename") f.HeaderComment("DO NOT EDIT: generated by grype/db/internal/codename/generate/main.go") f.ImportName(outputPackage, "pkg") f.Var().Id("normalizedOSCodenames").Op("=").Map(jen.String()).Map(jen.String()).Map(jen.String()).String().Values(jen.DictFunc(func(d jen.Dict) { for osName, versions := range osCodenames { majorMap := jen.Dict{} for major, minors := range versions { minorMap := jen.Dict{} for minor, codename := range minors { minorMap[jen.Lit(minor)] = jen.Lit(codename) } majorMap[jen.Lit(major)] = jen.Values(minorMap) } d[jen.Lit(osName)] = jen.Values(majorMap) } })) rendered := fmt.Sprintf("%#v", f) file, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { panic(fmt.Errorf("unable to open file: %w", err)) } defer file.Close() if _, err := file.WriteString(rendered); err != nil { panic(fmt.Errorf("unable to write file: %w", err)) } fmt.Printf("Code generation completed and written to %s\n", outputPath) } // fetchAndParse fetches the JSON data from a URL, parses it, and organizes it into a map. func fetchAndParse(url string, handler func(string) string) map[string]map[string]string { resp, err := http.Get(url) //nolint:gosec if err != nil { panic(fmt.Errorf("error fetching data from %s: %w", url, err)) } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { panic(fmt.Errorf("error reading response: %w", err)) } var versions []Version if err := json.Unmarshal(data, &versions); err != nil { panic(fmt.Errorf("error parsing JSON: %w", err)) } parsedData := make(map[string]map[string]string) for _, version := range versions { major, minor := parseVersion(version.Cycle) if parsedData[major] == nil { parsedData[major] = make(map[string]string) } codename := handler(version.Codename) fmt.Printf(" adding %s.%s --> %s\n", major, minor, codename) parsedData[major][minor] = codename } return parsedData } func lowercaseHandler(codename string) string { return strings.ToLower(codename) } func ubuntuHandler(codename string) string { return strings.ToLower(strings.Split(codename, " ")[0]) } // parseVersion splits a version string like "20.04" into major "20" and minor "04". func parseVersion(version string) (string, string) { parts := strings.Split(version, ".") major := strings.TrimLeft(parts[0], "0") minor := "*" if len(parts) > 1 { if parts[1] == "0" { minor = parts[1] } else { minor = strings.TrimLeft(parts[1], "0") } } return major, minor } ================================================ FILE: grype/db/internal/gormadapter/logger.go ================================================ package gormadapter import ( "context" "fmt" "time" "gorm.io/gorm/logger" anchoreLogger "github.com/anchore/go-logger" "github.com/anchore/grype/internal/log" ) // logAdapter is meant to adapt the gorm logger interface (see https://github.com/go-gorm/gorm/blob/v1.25.12/logger/logger.go) // to the anchore logger interface. type logAdapter struct { debug bool slowThreshold time.Duration level logger.LogLevel } // LogMode sets the log level for the logger and returns a new instance func (l *logAdapter) LogMode(level logger.LogLevel) logger.Interface { newlogger := *l newlogger.level = level return &newlogger } func (l logAdapter) Info(_ context.Context, fmt string, v ...interface{}) { if l.level >= logger.Info { if l.debug { log.Infof("[sql] "+fmt, v...) } } } func (l logAdapter) Warn(_ context.Context, fmt string, v ...interface{}) { if l.level >= logger.Warn { log.Warnf("[sql] "+fmt, v...) } } func (l logAdapter) Error(_ context.Context, fmt string, v ...interface{}) { if l.level >= logger.Error { log.Errorf("[sql] "+fmt, v...) } } // Trace logs the SQL statement and the duration it took to run the statement func (l logAdapter) Trace(_ context.Context, t time.Time, fn func() (sql string, rowsAffected int64), _ error) { if l.level <= logger.Silent { return } if l.debug { sql, rowsAffected := fn() elapsed := time.Since(t) fields := anchoreLogger.Fields{ "rows": rowsAffected, "duration": elapsed, } isSlow := l.slowThreshold != 0 && elapsed > l.slowThreshold if isSlow { fields["is-slow"] = isSlow fields["slow-threshold"] = fmt.Sprintf("> %s", l.slowThreshold) log.WithFields(fields).Warnf("[sql] %s", sql) } else { log.WithFields(fields).Tracef("[sql] %s", sql) } } } ================================================ FILE: grype/db/internal/gormadapter/open.go ================================================ package gormadapter import ( "fmt" "os" "path/filepath" "strings" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "github.com/anchore/grype/internal/log" ) var commonStatements = []string{ `PRAGMA foreign_keys = ON`, // needed for v6+ } var writerStatements = []string{ // performance improvements (note: will result in lost data on write interruptions) `PRAGMA synchronous = OFF`, // minimize the amount of syncing to disk, prioritizing write performance over durability `PRAGMA journal_mode = MEMORY`, // do not write the journal to disk (maximizing write performance); OFF is faster but less safe in terms of DB consistency } var heavyWriteStatements = []string{ `PRAGMA cache_size = -1073741824`, // ~1 GB (negative means treat as bytes not page count); one caveat is to not pick a value that risks swapping behavior, negating performance gains `PRAGMA mmap_size = 1073741824`, // ~1 GB; the maximum size of the memory-mapped I/O buffer (to access the database file as if it were a part of the process’s virtual memory) `PRAGMA defer_foreign_keys = ON`, // defer enforcement of foreign key constraints until the end of the transaction (to avoid the overhead of checking constraints for each row) } var readConnectionOptions = []string{ "immutable=1", // indicates that the database file is guaranteed not to change during the connection’s lifetime (slight performance benefit for read-only cases) "mode=ro", // opens the database in as read-only (an enforcement mechanism to allow immutable=1 to be effective) "cache=shared", // multiple database connections within the same process share a single page cache } type config struct { debug bool path string writable bool truncate bool allowLargeMemoryFootprint bool models []any initialData []any memory bool statements []string } type Option func(*config) func WithDebug(debug bool) Option { return func(c *config) { c.debug = debug } } func WithTruncate(truncate bool, models []any, initialData []any) Option { return func(c *config) { c.truncate = truncate if truncate { c.writable = true c.models = models c.initialData = initialData c.allowLargeMemoryFootprint = true } } } func WithStatements(statements ...string) Option { return func(c *config) { c.statements = append(c.statements, statements...) } } func WithModels(models []any) Option { return func(c *config) { c.models = append(c.models, models...) } } func WithWritable(write bool, models []any) Option { return func(c *config) { c.writable = write c.models = models } } func WithLargeMemoryFootprint(largeFootprint bool) Option { return func(c *config) { c.allowLargeMemoryFootprint = largeFootprint } } func newConfig(path string, opts []Option) config { c := config{} c.apply(path, opts) return c } func (c *config) apply(path string, opts []Option) { for _, o := range opts { o(c) } c.memory = len(path) == 0 c.path = path } func (c config) connectionString() string { var conn string if c.path == "" { conn = ":memory:" } else { conn = fmt.Sprintf("file:%s?cache=shared", c.path) } if !c.writable && !c.memory { if !strings.Contains(conn, "?") { conn += "?" } for _, o := range readConnectionOptions { conn += fmt.Sprintf("&%s", o) } } return conn } // Open a new connection to a sqlite3 database file func Open(path string, options ...Option) (*gorm.DB, error) { cfg := newConfig(path, options) if cfg.truncate && !cfg.writable { return nil, fmt.Errorf("cannot truncate a read-only DB") } if cfg.truncate { if err := deleteDB(path); err != nil { return nil, err } } dbObj, err := gorm.Open(sqlite.Open(cfg.connectionString()), &gorm.Config{Logger: &logAdapter{ debug: cfg.debug, slowThreshold: 400 * time.Millisecond, }}) if err != nil { return nil, fmt.Errorf("unable to connect to DB: %w", err) } return cfg.prepareDB(dbObj) } func (c config) prepareDB(dbObj *gorm.DB) (*gorm.DB, error) { if c.writable { log.WithFields("path", c.path).Debug("using writable DB statements") if err := c.applyStatements(dbObj, writerStatements); err != nil { return nil, fmt.Errorf("unable to apply DB writer statements: %w", err) } } if c.truncate && c.allowLargeMemoryFootprint { log.WithFields("path", c.path).Debug("using large memory footprint DB statements") if err := c.applyStatements(dbObj, heavyWriteStatements); err != nil { return nil, fmt.Errorf("unable to apply DB heavy writer statements: %w", err) } } if len(commonStatements) > 0 { if err := c.applyStatements(dbObj, commonStatements); err != nil { return nil, fmt.Errorf("unable to apply DB common statements: %w", err) } } if len(c.statements) > 0 { if err := c.applyStatements(dbObj, c.statements); err != nil { return nil, fmt.Errorf("unable to apply DB custom statements: %w", err) } } if len(c.models) > 0 && c.writable { log.WithFields("path", c.path).Debug("applying DB migrations") if err := dbObj.AutoMigrate(c.models...); err != nil { return nil, fmt.Errorf("unable to migrate: %w", err) } // now that there are potentially new models and indexes, analyze the DB to ensure the query planner is up-to-date if err := dbObj.Exec("ANALYZE").Error; err != nil { return nil, fmt.Errorf("unable to analyze DB: %w", err) } } if len(c.initialData) > 0 && c.truncate { log.WithFields("path", c.path).Debug("writing initial data") for _, d := range c.initialData { if err := dbObj.Create(d).Error; err != nil { return nil, fmt.Errorf("unable to create initial data: %w", err) } } } if c.debug { dbObj = dbObj.Debug() } return dbObj, nil } func (c config) applyStatements(db *gorm.DB, statements []string) error { for _, sqlStmt := range statements { if err := db.Exec(sqlStmt).Error; err != nil { return fmt.Errorf("unable to execute (%s): %w", sqlStmt, err) } if strings.HasPrefix(sqlStmt, "PRAGMA") { name, value, err := c.pragmaNameValue(sqlStmt) if err != nil { return fmt.Errorf("unable to parse PRAGMA statement: %w", err) } var result string if err := db.Raw("PRAGMA " + name + ";").Scan(&result).Error; err != nil { return fmt.Errorf("unable to verify PRAGMA %q: %w", name, err) } if !strings.EqualFold(result, value) { if value == "ON" && result == "1" { continue } if value == "OFF" && result == "0" { continue } return fmt.Errorf("PRAGMA %q was not set to %q (%q)", name, value, result) } } } return nil } func (c config) pragmaNameValue(sqlStmt string) (string, string, error) { sqlStmt = strings.TrimSuffix(strings.TrimSpace(sqlStmt), ";") // remove the trailing semicolon if strings.Count(sqlStmt, ";") > 0 { return "", "", fmt.Errorf("PRAGMA statements should not contain semicolons: %q", sqlStmt) } // check if the pragma was set, parse the pragma name and value from the statement. This is because // sqlite will not return errors when there are issues with the pragma key or value, but it will // be inconsistent with the expected value if you explicitly check var name, value string clean := strings.TrimPrefix(sqlStmt, "PRAGMA") fields := strings.SplitN(clean, "=", 2) if len(fields) == 2 { name = strings.ToLower(strings.TrimSpace(fields[0])) value = strings.TrimSpace(fields[1]) } else { return "", "", fmt.Errorf("unable to parse PRAGMA statement: %q", sqlStmt) } if c.memory && name == "mmap_size" { // memory only DBs do not have mmap capability value = "" } if name == "" { return "", "", fmt.Errorf("unable to parse name from PRAGMA statement: %q", sqlStmt) } return name, value, nil } func deleteDB(path string) error { if _, err := os.Stat(path); err == nil { if err := os.Remove(path); err != nil { return fmt.Errorf("unable to remove existing DB file: %w", err) } } parent := filepath.Dir(path) if err := os.MkdirAll(parent, 0700); err != nil { return fmt.Errorf("unable to create parent directory %q for DB file: %w", parent, err) } return nil } ================================================ FILE: grype/db/internal/gormadapter/open_test.go ================================================ package gormadapter import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestConfigApply(t *testing.T) { tests := []struct { name string path string options []Option expectedPath string expectedMemory bool }{ { name: "apply with path", path: "test.db", options: []Option{}, expectedPath: "test.db", expectedMemory: false, }, { name: "apply with empty path (memory)", path: "", options: []Option{}, expectedPath: "", expectedMemory: true, }, { name: "apply with truncate option", path: "test.db", options: []Option{WithTruncate(true, nil, nil)}, // migration and initial data don't matter expectedPath: "test.db", expectedMemory: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := newConfig(tt.path, tt.options) require.Equal(t, tt.expectedPath, c.path) require.Equal(t, tt.expectedMemory, c.memory) }) } } func TestConfigConnectionString(t *testing.T) { tests := []struct { name string path string write bool memory bool expectedConnStr string }{ { name: "writable path", path: "test.db", write: true, expectedConnStr: "file:test.db?cache=shared", }, { name: "read-only path", path: "test.db", write: false, expectedConnStr: "file:test.db?cache=shared&immutable=1&mode=ro&cache=shared", }, { name: "in-memory mode", path: "", write: false, memory: true, expectedConnStr: ":memory:", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := config{ path: tt.path, writable: tt.write, memory: tt.memory, } require.Equal(t, tt.expectedConnStr, c.connectionString()) }) } } func TestPrepareWritableDB(t *testing.T) { t.Run("creates new directory and file when path does not exist", func(t *testing.T) { tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "newdir", "test.db") err := deleteDB(dbPath) require.NoError(t, err) _, err = os.Stat(filepath.Dir(dbPath)) require.NoError(t, err) }) t.Run("removes existing file at path", func(t *testing.T) { tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test.db") _, err := os.Create(dbPath) require.NoError(t, err) _, err = os.Stat(dbPath) require.NoError(t, err) err = deleteDB(dbPath) require.NoError(t, err) _, err = os.Stat(dbPath) require.True(t, os.IsNotExist(err)) }) t.Run("returns error if unable to create parent directory", func(t *testing.T) { invalidDir := filepath.Join("/root", "invalidDir", "test.db") err := deleteDB(invalidDir) require.Error(t, err) require.Contains(t, err.Error(), "unable to create parent directory") }) } func TestPragmaNameValue(t *testing.T) { tests := []struct { name string cfg config input string wantName string wantValue string wantErr require.ErrorAssertionFunc }{ { name: "basic pragma", cfg: config{memory: false}, input: "PRAGMA journal_mode=WAL", wantName: "journal_mode", wantValue: "WAL", }, { name: "pragma with spaces", cfg: config{memory: false}, input: "PRAGMA cache_size = 2000 ", wantName: "cache_size", wantValue: "2000", }, { name: "pragma with trailing semicolon", cfg: config{memory: false}, input: "PRAGMA synchronous=NORMAL;", wantName: "synchronous", wantValue: "NORMAL", }, { name: "pragma with multiple semicolons", cfg: config{memory: false}, input: "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;", wantErr: require.Error, }, { name: "invalid pragma format", cfg: config{memory: false}, input: "PRAGMA invalid_format", wantErr: require.Error, }, { name: "mmap_size pragma with memory DB", cfg: config{memory: true}, input: "PRAGMA mmap_size=1000", wantName: "mmap_size", wantValue: "", // should be empty for memory DB }, { name: "mmap_size pragma with regular DB", cfg: config{memory: false}, input: "PRAGMA mmap_size=1000", wantName: "mmap_size", wantValue: "1000", }, { name: "pragma with numeric value", cfg: config{memory: false}, input: "PRAGMA page_size=4096", wantName: "page_size", wantValue: "4096", }, { name: "pragma with mixed case", cfg: config{memory: false}, input: "PRAGMA Journal_Mode=WAL", wantName: "journal_mode", wantValue: "WAL", }, { name: "empty pragma", cfg: config{memory: false}, input: "PRAGMA =value", wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } gotName, gotValue, err := tt.cfg.pragmaNameValue(tt.input) tt.wantErr(t, err) if err == nil { require.Equal(t, tt.wantName, gotName) require.Equal(t, tt.wantValue, gotValue) } }) } } ================================================ FILE: grype/db/internal/provider/unmarshal/annotated_openvex_vulnerability.go ================================================ package unmarshal import ( "io" govex "github.com/openvex/go-vex/pkg/vex" ) type AnnotatedOpenVEXVulnerability struct { Document govex.Statement `json:"document"` Fixes []AnnotatedOpenVEXFix `json:"fixes"` } type AnnotatedOpenVEXFix struct { Available AnnotatedOpenVEXFixAvailability `json:"available"` Product string `json:"product"` } type AnnotatedOpenVEXFixAvailability struct { Date string `json:"date"` Kind string `json:"kind"` } func AnnotatedOpenVEXVulnerabilityEntries(reader io.Reader) ([]AnnotatedOpenVEXVulnerability, error) { return unmarshalSingleOrMulti[AnnotatedOpenVEXVulnerability](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/eol.go ================================================ package unmarshal import "io" // EndOfLifeDateRelease represents a single release entry from the endoflife.date API v1. // This matches the ProductRelease schema with camelCase field names. // Ref: https://endoflife.date/api/v1/products/{product} // // Note: Product and Identifiers are denormalized from the parent product by vunnel. type EndOfLifeDateRelease struct { // Denormalized fields added by vunnel Product string `json:"product"` Identifiers []EndOfLifeDateIdentifier `json:"identifiers"` // Fields from endoflife.date ProductRelease schema Name string `json:"name"` Codename *string `json:"codename"` Label string `json:"label"` ReleaseDate *string `json:"releaseDate"` IsLTS bool `json:"isLts"` LTSFrom *string `json:"ltsFrom"` IsEOAS bool `json:"isEoas"` EOASFrom *string `json:"eoasFrom"` IsEOL bool `json:"isEol"` EOLFrom *string `json:"eolFrom"` IsMaintained bool `json:"isMaintained"` Latest *EndOfLifeDateLatest `json:"latest"` Custom map[string]interface{} `json:"custom"` } // EndOfLifeDateLatest represents the latest release info nested within a release. type EndOfLifeDateLatest struct { Name string `json:"name"` Date *string `json:"date"` Link *string `json:"link"` } // EndOfLifeDateIdentifier represents a CPE, PURL, or Repology identifier. type EndOfLifeDateIdentifier struct { Type string `json:"type"` ID string `json:"id"` } // EndOfLifeDateLabels contains custom labels for EOL-related dates. type EndOfLifeDateLabels struct { EOAS *string `json:"eoas"` Discontinued *string `json:"discontinued"` EOL *string `json:"eol"` EOES *string `json:"eoes"` } // EndOfLifeDateLinks contains URLs for the product. type EndOfLifeDateLinks struct { Icon *string `json:"icon"` HTML *string `json:"html"` ReleasePolicy *string `json:"releasePolicy"` } // EndOfLifeDateResult represents the result object within a product response. type EndOfLifeDateResult struct { Name string `json:"name"` Aliases []string `json:"aliases"` Label string `json:"label"` Category string `json:"category"` Tags []string `json:"tags"` VersionCommand *string `json:"versionCommand"` Identifiers []EndOfLifeDateIdentifier `json:"identifiers"` Labels EndOfLifeDateLabels `json:"labels"` Links EndOfLifeDateLinks `json:"links"` Releases []EndOfLifeDateRelease `json:"releases"` } // EndOfLifeDateProduct represents the full product response from the endoflife.date API v1. type EndOfLifeDateProduct struct { SchemaVersion string `json:"schema_version"` GeneratedAt string `json:"generated_at"` LastModified string `json:"last_modified"` Result EndOfLifeDateResult `json:"result"` } // IsEmpty returns true if the release has no meaningful data. func (e EndOfLifeDateRelease) IsEmpty() bool { return e.Product == "" } // ProductName returns the product name from the release. func (e EndOfLifeDateRelease) ProductName() string { return e.Product } // EndOfLifeDateReleaseEntries unmarshals EndOfLifeDateRelease records from a reader. func EndOfLifeDateReleaseEntries(reader io.Reader) ([]EndOfLifeDateRelease, error) { return unmarshalSingleOrMulti[EndOfLifeDateRelease](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/epss.go ================================================ package unmarshal import "io" type EPSS struct { CVE string `json:"cve"` EPSS float64 `json:"epss"` Percentile float64 `json:"percentile"` Date string `json:"date"` } func (o EPSS) IsEmpty() bool { return o.CVE == "" } func EPSSEntries(reader io.Reader) ([]EPSS, error) { return unmarshalSingleOrMulti[EPSS](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/errors.go ================================================ package unmarshal import ( "encoding/json" "fmt" ) func handleJSONUnmarshalError(err error) error { if ute, ok := err.(*json.UnmarshalTypeError); ok { //nolint: errorlint return fmt.Errorf("unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset) } else if se, ok := err.(*json.SyntaxError); ok { //nolint: errorlint return fmt.Errorf("syntax error: offset=%v, error=%w", se.Offset, se) } return err } ================================================ FILE: grype/db/internal/provider/unmarshal/github_advisory.go ================================================ package unmarshal import ( "io" ) type GitHubAdvisory struct { Advisory struct { Classification string CVE []string `json:"CVE"` CVSS *struct { BaseMetrics struct { BaseScore float64 `json:"base_score"` BaseSeverity string `json:"base_severity"` ExploitabilityScore float64 `json:"exploitability_score"` ImpactScore float64 `json:"impact_score"` } `json:"base_metrics"` Status string `json:"status"` VectorString string `json:"vector_string"` Version string `json:"version"` } `json:"CVSS"` CVSSSeverities []*struct { Vector string `json:"vector"` Version string `json:"version"` } `json:"cvss_severities"` FixedIn []GithubFixedIn `json:"FixedIn"` Metadata struct { CVE []string `json:"CVE"` } `json:"Metadata"` Severity string `json:"Severity"` Summary string `json:"Summary"` GhsaID string `json:"ghsaId"` Namespace string `json:"namespace"` URL string `json:"url"` Published string `json:"published"` Updated string `json:"updated"` Withdrawn string `json:"withdrawn"` References []*struct { URL string `json:"url"` } `json:"references"` } `json:"Advisory"` } func (g GitHubAdvisory) IsEmpty() bool { return g.Advisory.GhsaID == "" } func GitHubAdvisoryEntries(reader io.Reader) ([]GitHubAdvisory, error) { return unmarshalSingleOrMulti[GitHubAdvisory](reader) } type GithubFixedIn struct { Ecosystem string `json:"ecosystem"` Identifier string `json:"identifier"` Name string `json:"name"` Namespace string `json:"namespace"` Range string `json:"range"` Available struct { Date string `json:"date,omitempty"` Kind string `json:"kind,omitempty"` } `json:"available,omitempty"` } ================================================ FILE: grype/db/internal/provider/unmarshal/items_envelope.go ================================================ package unmarshal import ( "encoding/json" "fmt" "io" ) type ItemsEnvelope struct { Schema string `yaml:"schema" json:"schema" mapstructure:"schema"` Identifier string `yaml:"identifier" json:"identifier" mapstructure:"identifier"` Item json.RawMessage `yaml:"item" json:"item" mapstructure:"item"` } func Envelope(reader io.Reader) (*ItemsEnvelope, error) { var envelope ItemsEnvelope dec := json.NewDecoder(reader) err := dec.Decode(&envelope) if err != nil { return nil, fmt.Errorf("unable to open envelope: %w", err) } return &envelope, nil } ================================================ FILE: grype/db/internal/provider/unmarshal/known_exploited_vulnerability.go ================================================ package unmarshal import "io" type KnownExploitedVulnerability struct { CveID string `json:"cveID"` VendorProject string `json:"vendorProject"` Product string `json:"product"` VulnerabilityName string `json:"vulnerabilityName"` DateAdded string `json:"dateAdded"` ShortDescription string `json:"shortDescription"` RequiredAction string `json:"requiredAction"` DueDate string `json:"dueDate"` KnownRansomwareCampaignUse string `json:"knownRansomwareCampaignUse"` Notes string `json:"notes"` CWEs []string `json:"cwes"` } func (g KnownExploitedVulnerability) IsEmpty() bool { return g.CveID == "" } func KnownExploitedVulnerabilityEntries(reader io.Reader) ([]KnownExploitedVulnerability, error) { return unmarshalSingleOrMulti[KnownExploitedVulnerability](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/match_exclusion.go ================================================ package unmarshal import ( "io" ) type MatchExclusion struct { ID string `json:"id"` Constraints []struct { Vulnerability struct { Namespace string `json:"namespace,omitempty"` FixState string `json:"fix_state,omitempty"` } `json:"vulnerability,omitempty"` Package struct { Language string `json:"language,omitempty"` Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` Location string `json:"location,omitempty"` } `json:"package,omitempty"` } `json:"constraints,omitempty"` Justification string `json:"justification"` } func (m MatchExclusion) IsEmpty() bool { return m.ID == "" } func MatchExclusions(reader io.Reader) ([]MatchExclusion, error) { return unmarshalSingleOrMulti[MatchExclusion](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/msrc_vulnerability.go ================================================ package unmarshal import ( "io" ) // MSRCVulnerability represents a single Msrc entry with vulnerability metadata type MSRCVulnerability struct { Cvss struct { BaseScore float64 `json:"base_score"` TemporalScore float64 `json:"temporal_score"` Vector string `json:"vector"` } `json:"cvss"` FixedIn []struct { ID string `json:"id"` IsFirst bool `json:"is_first"` IsLatest bool `json:"is_latest"` Links []string `json:"links"` Available struct { Date string `json:"date,omitempty"` Kind string `json:"kind,omitempty"` } `json:"available,omitempty"` } `json:"fixed_in"` ID string `json:"id"` Link string `json:"link"` Product struct { Family string `json:"family"` ID string `json:"id"` Name string `json:"name"` } `json:"product"` Severity string `json:"severity"` Summary string `json:"summary"` Vulnerable []string `json:"vulnerable"` } func (o MSRCVulnerability) IsEmpty() bool { return o.ID == "" } func MSRCVulnerabilityEntries(reader io.Reader) ([]MSRCVulnerability, error) { return unmarshalSingleOrMulti[MSRCVulnerability](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/nvd/cve.go ================================================ package nvd import ( "sort" "github.com/Masterminds/semver/v3" "github.com/jinzhu/copier" "golang.org/x/text/cases" "golang.org/x/text/language" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss20" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss30" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss31" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss40" ) // note: this was autogenerated with some manual tweaking (see schema/nvd/cve-api-json/README.md) type Operator string const ( And Operator = "AND" Or Operator = "OR" ) const englishLanguage = "en" // this is the struct to use when unmarshalling directly from the API (which grype-db is NOT doing) // type APIResults struct { // Format string `json:"format"` // ResultsPerPage int64 `json:"resultsPerPage"` // StartIndex int64 `json:"startIndex"` // Timestamp string `json:"timestamp"` // TotalResults int64 `json:"totalResults"` // Version string `json:"version"` // Vulnerabilities []Vulnerability `json:"vulnerabilities"` //} type Vulnerability struct { Cve CveItem `json:"cve"` } type CveItem struct { ID string `json:"id"` // CisaActionDue *string `json:"cisaActionDue,omitempty"` // CisaExploitAdd *string `json:"cisaExploitAdd,omitempty"` // CisaRequiredAction *string `json:"cisaRequiredAction,omitempty"` // CisaVulnerabilityName *string `json:"cisaVulnerabilityName,omitempty"` Configurations []Configuration `json:"configurations,omitempty"` Descriptions []LangString `json:"descriptions"` // EvaluatorComment *string `json:"evaluatorComment,omitempty"` // EvaluatorImpact *string `json:"evaluatorImpact,omitempty"` // EvaluatorSolution *string `json:"evaluatorSolution,omitempty"` LastModified string `json:"lastModified"` Metrics *Metrics `json:"metrics,omitempty"` Published string `json:"published"` References []Reference `json:"references"` SourceIdentifier *string `json:"sourceIdentifier,omitempty"` // VendorComments []VendorComment `json:"vendorComments,omitempty"` VulnStatus *string `json:"vulnStatus,omitempty"` Weaknesses []Weakness `json:"weaknesses,omitempty"` } type Configuration struct { Negate *bool `json:"negate,omitempty"` Nodes []Node `json:"nodes"` Operator *Operator `json:"operator,omitempty"` } type Node struct { CpeMatch []CpeMatch `json:"cpeMatch"` Negate *bool `json:"negate,omitempty"` Operator Operator `json:"operator"` } type FixInfo struct { Version string `json:"version"` Date string `json:"date"` Kind string `json:"kind"` } type CpeMatch struct { Criteria string `json:"criteria"` MatchCriteriaID string `json:"matchCriteriaId"` VersionEndExcluding *string `json:"versionEndExcluding,omitempty"` VersionEndIncluding *string `json:"versionEndIncluding,omitempty"` VersionStartExcluding *string `json:"versionStartExcluding,omitempty"` VersionStartIncluding *string `json:"versionStartIncluding,omitempty"` Vulnerable bool `json:"vulnerable"` Fix *FixInfo `json:"fix,omitempty"` } type LangString struct { Lang string `json:"lang"` Value string `json:"value"` } // Metrics scores for a vulnerability as found on NVD. type Metrics struct { CvssMetricV2 []CvssV2 `json:"cvssMetricV2,omitempty"` // CVSS V2.0 score. CvssMetricV30 []CvssV30 `json:"cvssMetricV30,omitempty"` // CVSS V3.0 score. CvssMetricV31 []CvssV31 `json:"cvssMetricV31,omitempty"` // CVSS V3.1 score. CvssMetricV40 []CvssV40 `json:"cvssMetricV40,omitempty"` // CVSS V4.1 score. } type CvssV2 struct { // ACInsufInfo *bool `json:"acInsufInfo,omitempty"` BaseSeverity *string `json:"baseSeverity,omitempty"` CvssData cvss20.Cvss20 `json:"cvssData"` ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` ImpactScore *float64 `json:"impactScore,omitempty"` // ObtainAllPrivilege *bool `json:"obtainAllPrivilege,omitempty"` // ObtainOtherPrivilege *bool `json:"obtainOtherPrivilege,omitempty"` // ObtainUserPrivilege *bool `json:"obtainUserPrivilege,omitempty"` Source string `json:"source"` Type CvssType `json:"type"` // UserInteractionRequired *bool `json:"userInteractionRequired,omitempty"` } type CvssV30 struct { CvssData cvss30.Cvss30 `json:"cvssData"` ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` ImpactScore *float64 `json:"impactScore,omitempty"` Source string `json:"source"` Type CvssType `json:"type"` } type CvssV31 struct { CvssData cvss31.Cvss31 `json:"cvssData"` ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` ImpactScore *float64 `json:"impactScore,omitempty"` Source string `json:"source"` Type CvssType `json:"type"` } type CvssV40 struct { CvssData cvss40.Cvss40 `json:"cvssData"` ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` ImpactScore *float64 `json:"impactScore,omitempty"` Source string `json:"source"` Type CvssType `json:"type"` } // CvssType relative to the NVD docs: "type identifies whether the organization is a primary or secondary source. // Primary sources include the NVD and CNA who have reached the provider level in CVMAP. 10% of provider level // submissions are audited by the NVD. If a submission has been audited the NVD will appear as the primary source // and the provider level CNA will appear as the secondary source." type CvssType string const ( Primary CvssType = "Primary" Secondary CvssType = "Secondary" ) type Reference struct { Source *string `json:"source,omitempty"` Tags []string `json:"tags,omitempty"` URL string `json:"url"` } // type VendorComment struct { // Comment string `json:"comment"` // LastModified string `json:"lastModified"` // Organization string `json:"organization"` // } type Weakness struct { Description []LangString `json:"description"` Source string `json:"source"` Type string `json:"type"` } func (o CveItem) Description() string { for _, d := range o.Descriptions { if d.Lang == englishLanguage { return d.Value } } return "" } type CvssSummary struct { Source string Type CvssType Version string Vector string BaseScore float64 ExploitabilityScore *float64 ImpactScore *float64 baseSeverity *string } func (o CvssSummary) Severity() string { if o.baseSeverity != nil { return cases.Title(language.English).String(*o.baseSeverity) } return "" } func (o CvssSummary) version() *semver.Version { v, err := semver.NewVersion(o.Version) if err != nil { return semver.MustParse("2.0") } return v } type CvssSummaries []CvssSummary func (o CvssSummaries) Len() int { return len(o) } func (o CvssSummaries) Less(i, j int) bool { iEntry := o[i] jEntry := o[j] // first compare by type (Primary/Secondary) if iEntry.Type != jEntry.Type { return iEntry.Type == Secondary } // then compare by source (NVD preferred, then lexicographic) if iEntry.Source != jEntry.Source { if iEntry.Source == "nvd@nist.gov" { return false } if jEntry.Source == "nvd@nist.gov" { return true } // for non-NVD sources, use lexicographic ordering (descending for Reverse sort) return iEntry.Source > jEntry.Source } // finally, compare by version when type and source are the same (v4 > v3 > v2 > v1) iV := iEntry.version() jV := jEntry.version() return iV.LessThan(jV) } func (o CvssSummaries) Swap(i, j int) { o[i], o[j] = o[j], o[i] } func (o CvssSummaries) Severity() string { for _, c := range o { sev := c.Severity() if sev != "" { return sev } } return "" } func (o CvssSummaries) Sorted() CvssSummaries { var n CvssSummaries if err := copier.Copy(&n, &o); err != nil { panic(err) } sort.Sort(sort.Reverse(n)) return n } func (o CveItem) CVSS() []CvssSummary { if o.Metrics == nil { return nil } var results CvssSummaries for _, c := range o.Metrics.CvssMetricV2 { results = append(results, CvssSummary{ Source: c.Source, Type: c.Type, Version: c.CvssData.Version, Vector: c.CvssData.VectorString, BaseScore: c.CvssData.BaseScore, ExploitabilityScore: c.ExploitabilityScore, ImpactScore: c.ImpactScore, baseSeverity: c.BaseSeverity, }, ) } for _, c := range o.Metrics.CvssMetricV30 { sev := string(c.CvssData.BaseSeverity) results = append(results, CvssSummary{ Source: c.Source, Type: c.Type, Version: c.CvssData.Version, Vector: c.CvssData.VectorString, BaseScore: c.CvssData.BaseScore, ExploitabilityScore: c.ExploitabilityScore, ImpactScore: c.ImpactScore, baseSeverity: &sev, }, ) } for _, c := range o.Metrics.CvssMetricV31 { sev := string(c.CvssData.BaseSeverity) results = append(results, CvssSummary{ Source: c.Source, Type: c.Type, Version: c.CvssData.Version, Vector: c.CvssData.VectorString, BaseScore: c.CvssData.BaseScore, ExploitabilityScore: c.ExploitabilityScore, ImpactScore: c.ImpactScore, baseSeverity: &sev, }, ) } for _, c := range o.Metrics.CvssMetricV40 { sev := string(c.CvssData.BaseSeverity) results = append(results, CvssSummary{ Source: c.Source, Type: c.Type, Version: c.CvssData.Version, Vector: c.CvssData.VectorString, BaseScore: c.CvssData.BaseScore, ExploitabilityScore: c.ExploitabilityScore, ImpactScore: c.ImpactScore, baseSeverity: &sev, }, ) } return results } func (o Vulnerability) IsEmpty() bool { return o.Cve.ID == "" } ================================================ FILE: grype/db/internal/provider/unmarshal/nvd/cve_test.go ================================================ package nvd import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) func TestCvssSummariesSorted(t *testing.T) { tests := []struct { name string input CvssSummaries expected CvssSummaries }{ { name: "primary types sorted by version descending", input: CvssSummaries{ {Type: Primary, Version: "2.0", Source: "same-source"}, {Type: Primary, Version: "3.1", Source: "same-source"}, {Type: Primary, Version: "3.0", Source: "same-source"}, {Type: Primary, Version: "4.0", Source: "same-source"}, }, expected: CvssSummaries{ {Type: Primary, Version: "4.0", Source: "same-source"}, {Type: Primary, Version: "3.1", Source: "same-source"}, {Type: Primary, Version: "3.0", Source: "same-source"}, {Type: Primary, Version: "2.0", Source: "same-source"}, }, }, { name: "secondary types sorted by version descending", input: CvssSummaries{ {Type: Secondary, Version: "2.0", Source: "same-source"}, {Type: Secondary, Version: "3.1", Source: "same-source"}, {Type: Secondary, Version: "3.0", Source: "same-source"}, }, expected: CvssSummaries{ {Type: Secondary, Version: "3.1", Source: "same-source"}, {Type: Secondary, Version: "3.0", Source: "same-source"}, {Type: Secondary, Version: "2.0", Source: "same-source"}, }, }, { name: "primary types before secondary types", input: CvssSummaries{ {Type: Secondary, Version: "3.1", Source: "G"}, {Type: Primary, Version: "2.0", Source: "H"}, {Type: Secondary, Version: "2.0", Source: "I"}, {Type: Primary, Version: "3.0", Source: "J"}, }, expected: CvssSummaries{ {Type: Primary, Version: "2.0", Source: "H"}, {Type: Primary, Version: "3.0", Source: "J"}, {Type: Secondary, Version: "3.1", Source: "G"}, {Type: Secondary, Version: "2.0", Source: "I"}, }, }, { name: "mix of versions and types", input: CvssSummaries{ {Type: Secondary, Version: "3.1", Source: "K"}, {Type: Primary, Version: "3.1", Source: "L"}, {Type: Primary, Version: "2.0", Source: "M"}, {Type: Secondary, Version: "2.0", Source: "N"}, {Type: Primary, Version: "3.0", Source: "O"}, {Type: Secondary, Version: "3.0", Source: "P"}, }, expected: CvssSummaries{ {Type: Primary, Version: "3.1", Source: "L"}, {Type: Primary, Version: "2.0", Source: "M"}, {Type: Primary, Version: "3.0", Source: "O"}, {Type: Secondary, Version: "3.1", Source: "K"}, {Type: Secondary, Version: "2.0", Source: "N"}, {Type: Secondary, Version: "3.0", Source: "P"}, }, }, { name: "nvd source preferred within same type and version", input: CvssSummaries{ {Type: Primary, Version: "3.0", Source: "random-source"}, {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, }, expected: CvssSummaries{ {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "random-source"}, }, }, { name: "nvd source preferred but type takes precedence", input: CvssSummaries{ {Type: Secondary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "random-source"}, }, expected: CvssSummaries{ {Type: Primary, Version: "3.0", Source: "random-source"}, {Type: Secondary, Version: "3.0", Source: "nvd@nist.gov"}, }, }, { name: "multiple nvd sources sorted by version", input: CvssSummaries{ {Type: Primary, Version: "2.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.1", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, }, expected: CvssSummaries{ {Type: Primary, Version: "3.1", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "2.0", Source: "nvd@nist.gov"}, }, }, { name: "complex sorting with types, versions, and sources", input: CvssSummaries{ {Type: Secondary, Version: "3.1", Source: "nvd@nist.gov"}, {Type: Primary, Version: "2.0", Source: "random-source"}, {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "other-source"}, {Type: Secondary, Version: "2.0", Source: "other-source"}, {Type: Secondary, Version: "3.0", Source: "nvd@nist.gov"}, }, expected: CvssSummaries{ {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "other-source"}, {Type: Primary, Version: "2.0", Source: "random-source"}, {Type: Secondary, Version: "3.1", Source: "nvd@nist.gov"}, {Type: Secondary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Secondary, Version: "2.0", Source: "other-source"}, }, }, { name: "empty input", input: CvssSummaries{}, expected: CvssSummaries{}, }, { name: "invalid version handling", input: CvssSummaries{ {Type: Primary, Version: "invalid", Source: "Q"}, {Type: Primary, Version: "3.0", Source: "R"}, }, expected: CvssSummaries{ {Type: Primary, Version: "invalid", Source: "Q"}, // sorted by source (Q < R) {Type: Primary, Version: "3.0", Source: "R"}, }, }, { name: "source takes priority over version, then version as tiebreaker", input: CvssSummaries{ {Type: Primary, Version: "4.0", Source: "other-source"}, {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "2.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "3.0", Source: "source-a"}, }, expected: CvssSummaries{ {Type: Primary, Version: "3.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "2.0", Source: "nvd@nist.gov"}, {Type: Primary, Version: "4.0", Source: "other-source"}, {Type: Primary, Version: "3.0", Source: "source-a"}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := tc.input.Sorted() if d := cmp.Diff(tc.expected, result, cmpopts.IgnoreUnexported(CvssSummary{})); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) } } func TestCvssSummaryVersion(t *testing.T) { tests := []struct { input string expected string }{ {"4.0", "4.0.0"}, {"3.1", "3.1.0"}, {"3.0", "3.0.0"}, {"2.0", "2.0.0"}, {"invalid", "2.0.0"}, // default to 2.0 for invalid versions {"3.1.5", "3.1.5"}, {"", "2.0.0"}, // empty string is invalid } for _, tc := range tests { t.Run(tc.input, func(t *testing.T) { summary := CvssSummary{Version: tc.input} version := summary.version() if version.String() != tc.expected { t.Errorf("Expected version %s, got %s", tc.expected, version.String()) } }) } } ================================================ FILE: grype/db/internal/provider/unmarshal/nvd/cvss20/cvss20.go ================================================ package cvss20 // note: this was autogenerated with some manual tweaking type Cvss20 struct { // AccessComplexity *AccessComplexityType `json:"accessComplexity,omitempty"` // AccessVector *AccessVectorType `json:"accessVector,omitempty"` // Authentication *AuthenticationType `json:"authentication,omitempty"` // AvailabilityImpact *CiaType `json:"availabilityImpact,omitempty"` // AvailabilityRequirement *CiaRequirementType `json:"availabilityRequirement,omitempty"` BaseScore float64 `json:"baseScore"` // CollateralDamagePotential *CollateralDamagePotentialType `json:"collateralDamagePotential,omitempty"` // ConfidentialityImpact *CiaType `json:"confidentialityImpact,omitempty"` // ConfidentialityRequirement *CiaRequirementType `json:"confidentialityRequirement,omitempty"` // EnvironmentalScore *float64 `json:"environmentalScore,omitempty"` // Exploitability *ExploitabilityType `json:"exploitability,omitempty"` // IntegrityImpact *CiaType `json:"integrityImpact,omitempty"` // IntegrityRequirement *CiaRequirementType `json:"integrityRequirement,omitempty"` // RemediationLevel *RemediationLevelType `json:"remediationLevel,omitempty"` // ReportConfidence *ReportConfidenceType `json:"reportConfidence,omitempty"` // TargetDistribution *TargetDistributionType `json:"targetDistribution,omitempty"` // TemporalScore *float64 `json:"temporalScore,omitempty"` VectorString string `json:"vectorString"` Version string `json:"version"` // CVSS Version } // type AccessComplexityType string // // const ( // AccessComplexityTypeHIGH AccessComplexityType = "HIGH" // AccessComplexityTypeLOW AccessComplexityType = "LOW" // AccessComplexityTypeMEDIUM AccessComplexityType = "MEDIUM" //) // // type AccessVectorType string // // const ( // AdjacentNetwork AccessVectorType = "ADJACENT_NETWORK" // Local AccessVectorType = "LOCAL" // Network AccessVectorType = "NETWORK" //) // // type AuthenticationType string // // const ( // AuthenticationTypeNONE AuthenticationType = "NONE" // Multiple AuthenticationType = "MULTIPLE" // Single AuthenticationType = "SINGLE" //) // // type CiaType string // // const ( // CiaTypeNONE CiaType = "NONE" // Complete CiaType = "COMPLETE" // Partial CiaType = "PARTIAL" //) // // type CiaRequirementType string // // const ( // CiaRequirementTypeHIGH CiaRequirementType = "HIGH" // CiaRequirementTypeLOW CiaRequirementType = "LOW" // CiaRequirementTypeMEDIUM CiaRequirementType = "MEDIUM" // CiaRequirementTypeNOTDEFINED CiaRequirementType = "NOT_DEFINED" //) // // type CollateralDamagePotentialType string // // const ( // CollateralDamagePotentialTypeHIGH CollateralDamagePotentialType = "HIGH" // CollateralDamagePotentialTypeLOW CollateralDamagePotentialType = "LOW" // CollateralDamagePotentialTypeNONE CollateralDamagePotentialType = "NONE" // CollateralDamagePotentialTypeNOTDEFINED CollateralDamagePotentialType = "NOT_DEFINED" // LowMedium CollateralDamagePotentialType = "LOW_MEDIUM" // MediumHigh CollateralDamagePotentialType = "MEDIUM_HIGH" //) // // type ExploitabilityType string // // const ( // ExploitabilityTypeHIGH ExploitabilityType = "HIGH" // ExploitabilityTypeNOTDEFINED ExploitabilityType = "NOT_DEFINED" // Functional ExploitabilityType = "FUNCTIONAL" // ProofOfConcept ExploitabilityType = "PROOF_OF_CONCEPT" // Unproven ExploitabilityType = "UNPROVEN" //) // // type RemediationLevelType string // // const ( // OfficialFix RemediationLevelType = "OFFICIAL_FIX" // RemediationLevelTypeNOTDEFINED RemediationLevelType = "NOT_DEFINED" // TemporaryFix RemediationLevelType = "TEMPORARY_FIX" // Unavailable RemediationLevelType = "UNAVAILABLE" // Workaround RemediationLevelType = "WORKAROUND" //) // // type ReportConfidenceType string // // const ( // Confirmed ReportConfidenceType = "CONFIRMED" // ReportConfidenceTypeNOTDEFINED ReportConfidenceType = "NOT_DEFINED" // Unconfirmed ReportConfidenceType = "UNCONFIRMED" // Uncorroborated ReportConfidenceType = "UNCORROBORATED" //) // // type TargetDistributionType string // // const ( // TargetDistributionTypeHIGH TargetDistributionType = "HIGH" // TargetDistributionTypeLOW TargetDistributionType = "LOW" // TargetDistributionTypeMEDIUM TargetDistributionType = "MEDIUM" // TargetDistributionTypeNONE TargetDistributionType = "NONE" // TargetDistributionTypeNOTDEFINED TargetDistributionType = "NOT_DEFINED" //) ================================================ FILE: grype/db/internal/provider/unmarshal/nvd/cvss30/cvss30.go ================================================ package cvss30 // note: this was autogenerated with some manual tweaking type Cvss30 struct { // AttackComplexity *AttackComplexityType `json:"attackComplexity,omitempty"` // AttackVector *AttackVectorType `json:"attackVector,omitempty"` // AvailabilityImpact *Type `json:"availabilityImpact,omitempty"` // AvailabilityRequirement *CiaRequirementType `json:"availabilityRequirement,omitempty"` BaseScore float64 `json:"baseScore"` BaseSeverity SeverityType `json:"baseSeverity"` // ConfidentialityImpact *Type `json:"confidentialityImpact,omitempty"` // ConfidentialityRequirement *CiaRequirementType `json:"confidentialityRequirement,omitempty"` // EnvironmentalScore *float64 `json:"environmentalScore,omitempty"` // EnvironmentalSeverity *SeverityType `json:"environmentalSeverity,omitempty"` // ExploitCodeMaturity *ExploitCodeMaturityType `json:"exploitCodeMaturity,omitempty"` // IntegrityImpact *Type `json:"integrityImpact,omitempty"` // IntegrityRequirement *CiaRequirementType `json:"integrityRequirement,omitempty"` // ModifiedAttackComplexity *ModifiedAttackComplexityType `json:"modifiedAttackComplexity,omitempty"` // ModifiedAttackVector *ModifiedAttackVectorType `json:"modifiedAttackVector,omitempty"` // ModifiedAvailabilityImpact *ModifiedType `json:"modifiedAvailabilityImpact,omitempty"` // ModifiedConfidentialityImpact *ModifiedType `json:"modifiedConfidentialityImpact,omitempty"` // ModifiedIntegrityImpact *ModifiedType `json:"modifiedIntegrityImpact,omitempty"` // ModifiedPrivilegesRequired *ModifiedType `json:"modifiedPrivilegesRequired,omitempty"` // ModifiedScope *ModifiedScopeType `json:"modifiedScope,omitempty"` // ModifiedUserInteraction *ModifiedUserInteractionType `json:"modifiedUserInteraction,omitempty"` // PrivilegesRequired *Type `json:"privilegesRequired,omitempty"` // RemediationLevel *RemediationLevelType `json:"remediationLevel,omitempty"` // ReportConfidence *ConfidenceType `json:"reportConfidence,omitempty"` // Scope *ScopeType `json:"scope,omitempty"` // TemporalScore *float64 `json:"temporalScore,omitempty"` // TemporalSeverity *SeverityType `json:"temporalSeverity,omitempty"` // UserInteraction *UserInteractionType `json:"userInteraction,omitempty"` VectorString string `json:"vectorString"` Version string `json:"version"` // CVSS Version } // type AttackComplexityType string // // const ( // AttackComplexityTypeHIGH AttackComplexityType = "HIGH" // AttackComplexityTypeLOW AttackComplexityType = "LOW" //) // // type AttackVectorType string // // const ( // AttackVectorTypeADJACENTNETWORK AttackVectorType = "ADJACENT_NETWORK" // AttackVectorTypeLOCAL AttackVectorType = "LOCAL" // AttackVectorTypeNETWORK AttackVectorType = "NETWORK" // AttackVectorTypePHYSICAL AttackVectorType = "PHYSICAL" //) // // type Type string // // const ( // TypeHIGH Type = "HIGH" // TypeLOW Type = "LOW" // TypeNONE Type = "NONE" //) // // type CiaRequirementType string // // const ( // CiaRequirementTypeHIGH CiaRequirementType = "HIGH" // CiaRequirementTypeLOW CiaRequirementType = "LOW" // CiaRequirementTypeMEDIUM CiaRequirementType = "MEDIUM" // CiaRequirementTypeNOTDEFINED CiaRequirementType = "NOT_DEFINED" //) type SeverityType string const ( Critical SeverityType = "CRITICAL" SeverityTypeHIGH SeverityType = "HIGH" SeverityTypeLOW SeverityType = "LOW" SeverityTypeMEDIUM SeverityType = "MEDIUM" SeverityTypeNONE SeverityType = "NONE" ) // // type ExploitCodeMaturityType string // // const ( // ExploitCodeMaturityTypeHIGH ExploitCodeMaturityType = "HIGH" // ExploitCodeMaturityTypeNOTDEFINED ExploitCodeMaturityType = "NOT_DEFINED" // Functional ExploitCodeMaturityType = "FUNCTIONAL" // ProofOfConcept ExploitCodeMaturityType = "PROOF_OF_CONCEPT" // Unproven ExploitCodeMaturityType = "UNPROVEN" //) // // type ModifiedAttackComplexityType string // // const ( // ModifiedAttackComplexityTypeHIGH ModifiedAttackComplexityType = "HIGH" // ModifiedAttackComplexityTypeLOW ModifiedAttackComplexityType = "LOW" // ModifiedAttackComplexityTypeNOTDEFINED ModifiedAttackComplexityType = "NOT_DEFINED" //) // // type ModifiedAttackVectorType string // // const ( // ModifiedAttackVectorTypeADJACENTNETWORK ModifiedAttackVectorType = "ADJACENT_NETWORK" // ModifiedAttackVectorTypeLOCAL ModifiedAttackVectorType = "LOCAL" // ModifiedAttackVectorTypeNETWORK ModifiedAttackVectorType = "NETWORK" // ModifiedAttackVectorTypeNOTDEFINED ModifiedAttackVectorType = "NOT_DEFINED" // ModifiedAttackVectorTypePHYSICAL ModifiedAttackVectorType = "PHYSICAL" //) // // type ModifiedType string // // const ( // ModifiedTypeHIGH ModifiedType = "HIGH" // ModifiedTypeLOW ModifiedType = "LOW" // ModifiedTypeNONE ModifiedType = "NONE" // ModifiedTypeNOTDEFINED ModifiedType = "NOT_DEFINED" //) // // type ModifiedScopeType string // // const ( // ModifiedScopeTypeCHANGED ModifiedScopeType = "CHANGED" // ModifiedScopeTypeNOTDEFINED ModifiedScopeType = "NOT_DEFINED" // ModifiedScopeTypeUNCHANGED ModifiedScopeType = "UNCHANGED" //) // // type ModifiedUserInteractionType string // // const ( // ModifiedUserInteractionTypeNONE ModifiedUserInteractionType = "NONE" // ModifiedUserInteractionTypeNOTDEFINED ModifiedUserInteractionType = "NOT_DEFINED" // ModifiedUserInteractionTypeREQUIRED ModifiedUserInteractionType = "REQUIRED" //) // // type RemediationLevelType string // // const ( // OfficialFix RemediationLevelType = "OFFICIAL_FIX" // RemediationLevelTypeNOTDEFINED RemediationLevelType = "NOT_DEFINED" // TemporaryFix RemediationLevelType = "TEMPORARY_FIX" // Unavailable RemediationLevelType = "UNAVAILABLE" // Workaround RemediationLevelType = "WORKAROUND" //) // // type ConfidenceType string // // const ( // ConfidenceTypeNOTDEFINED ConfidenceType = "NOT_DEFINED" // Confirmed ConfidenceType = "CONFIRMED" // Reasonable ConfidenceType = "REASONABLE" // Unknown ConfidenceType = "UNKNOWN" //) // // type ScopeType string // // const ( // ScopeTypeCHANGED ScopeType = "CHANGED" // ScopeTypeUNCHANGED ScopeType = "UNCHANGED" //) // // type UserInteractionType string // // const ( // UserInteractionTypeNONE UserInteractionType = "NONE" // UserInteractionTypeREQUIRED UserInteractionType = "REQUIRED" //) ================================================ FILE: grype/db/internal/provider/unmarshal/nvd/cvss31/cvss31.go ================================================ package cvss31 // note: this was autogenerated with some manual tweaking type Cvss31 struct { // AttackComplexity *AttackComplexityType `json:"attackComplexity,omitempty"` // AttackVector *AttackVectorType `json:"attackVector,omitempty"` // AvailabilityImpact *Type `json:"availabilityImpact,omitempty"` // AvailabilityRequirement *CiaRequirementType `json:"availabilityRequirement,omitempty"` BaseScore float64 `json:"baseScore"` BaseSeverity SeverityType `json:"baseSeverity"` // ConfidentialityImpact *Type `json:"confidentialityImpact,omitempty"` // ConfidentialityRequirement *CiaRequirementType `json:"confidentialityRequirement,omitempty"` // EnvironmentalScore *float64 `json:"environmentalScore,omitempty"` // EnvironmentalSeverity *SeverityType `json:"environmentalSeverity,omitempty"` // ExploitCodeMaturity *ExploitCodeMaturityType `json:"exploitCodeMaturity,omitempty"` // IntegrityImpact *Type `json:"integrityImpact,omitempty"` // IntegrityRequirement *CiaRequirementType `json:"integrityRequirement,omitempty"` // ModifiedAttackComplexity *ModifiedAttackComplexityType `json:"modifiedAttackComplexity,omitempty"` // ModifiedAttackVector *ModifiedAttackVectorType `json:"modifiedAttackVector,omitempty"` // ModifiedAvailabilityImpact *ModifiedType `json:"modifiedAvailabilityImpact,omitempty"` // ModifiedConfidentialityImpact *ModifiedType `json:"modifiedConfidentialityImpact,omitempty"` // ModifiedIntegrityImpact *ModifiedType `json:"modifiedIntegrityImpact,omitempty"` // ModifiedPrivilegesRequired *ModifiedType `json:"modifiedPrivilegesRequired,omitempty"` // ModifiedScope *ModifiedScopeType `json:"modifiedScope,omitempty"` // ModifiedUserInteraction *ModifiedUserInteractionType `json:"modifiedUserInteraction,omitempty"` // PrivilegesRequired *Type `json:"privilegesRequired,omitempty"` // RemediationLevel *RemediationLevelType `json:"remediationLevel,omitempty"` // ReportConfidence *ConfidenceType `json:"reportConfidence,omitempty"` // Scope *ScopeType `json:"scope,omitempty"` // TemporalScore *float64 `json:"temporalScore,omitempty"` // TemporalSeverity *SeverityType `json:"temporalSeverity,omitempty"` // UserInteraction *UserInteractionType `json:"userInteraction,omitempty"` VectorString string `json:"vectorString"` Version string `json:"version"` // CVSS Version } // type AttackComplexityType string // // const ( // AttackComplexityTypeHIGH AttackComplexityType = "HIGH" // AttackComplexityTypeLOW AttackComplexityType = "LOW" //) // // type AttackVectorType string // // const ( // AttackVectorTypeADJACENTNETWORK AttackVectorType = "ADJACENT_NETWORK" // AttackVectorTypeLOCAL AttackVectorType = "LOCAL" // AttackVectorTypeNETWORK AttackVectorType = "NETWORK" // AttackVectorTypePHYSICAL AttackVectorType = "PHYSICAL" //) // // type Type string // // const ( // TypeHIGH Type = "HIGH" // TypeLOW Type = "LOW" // TypeNONE Type = "NONE" //) // // type CiaRequirementType string // // const ( // CiaRequirementTypeHIGH CiaRequirementType = "HIGH" // CiaRequirementTypeLOW CiaRequirementType = "LOW" // CiaRequirementTypeMEDIUM CiaRequirementType = "MEDIUM" // CiaRequirementTypeNOTDEFINED CiaRequirementType = "NOT_DEFINED" //) type SeverityType string const ( Critical SeverityType = "CRITICAL" SeverityTypeHIGH SeverityType = "HIGH" SeverityTypeLOW SeverityType = "LOW" SeverityTypeMEDIUM SeverityType = "MEDIUM" SeverityTypeNONE SeverityType = "NONE" ) // type ExploitCodeMaturityType string // // const ( // ExploitCodeMaturityTypeHIGH ExploitCodeMaturityType = "HIGH" // ExploitCodeMaturityTypeNOTDEFINED ExploitCodeMaturityType = "NOT_DEFINED" // Functional ExploitCodeMaturityType = "FUNCTIONAL" // ProofOfConcept ExploitCodeMaturityType = "PROOF_OF_CONCEPT" // Unproven ExploitCodeMaturityType = "UNPROVEN" //) // // type ModifiedAttackComplexityType string // // const ( // ModifiedAttackComplexityTypeHIGH ModifiedAttackComplexityType = "HIGH" // ModifiedAttackComplexityTypeLOW ModifiedAttackComplexityType = "LOW" // ModifiedAttackComplexityTypeNOTDEFINED ModifiedAttackComplexityType = "NOT_DEFINED" //) // // type ModifiedAttackVectorType string // // const ( // ModifiedAttackVectorTypeADJACENTNETWORK ModifiedAttackVectorType = "ADJACENT_NETWORK" // ModifiedAttackVectorTypeLOCAL ModifiedAttackVectorType = "LOCAL" // ModifiedAttackVectorTypeNETWORK ModifiedAttackVectorType = "NETWORK" // ModifiedAttackVectorTypeNOTDEFINED ModifiedAttackVectorType = "NOT_DEFINED" // ModifiedAttackVectorTypePHYSICAL ModifiedAttackVectorType = "PHYSICAL" //) // // type ModifiedType string // // const ( // ModifiedTypeHIGH ModifiedType = "HIGH" // ModifiedTypeLOW ModifiedType = "LOW" // ModifiedTypeNONE ModifiedType = "NONE" // ModifiedTypeNOTDEFINED ModifiedType = "NOT_DEFINED" //) // // type ModifiedScopeType string // // const ( // ModifiedScopeTypeCHANGED ModifiedScopeType = "CHANGED" // ModifiedScopeTypeNOTDEFINED ModifiedScopeType = "NOT_DEFINED" // ModifiedScopeTypeUNCHANGED ModifiedScopeType = "UNCHANGED" //) // // type ModifiedUserInteractionType string // // const ( // ModifiedUserInteractionTypeNONE ModifiedUserInteractionType = "NONE" // ModifiedUserInteractionTypeNOTDEFINED ModifiedUserInteractionType = "NOT_DEFINED" // ModifiedUserInteractionTypeREQUIRED ModifiedUserInteractionType = "REQUIRED" //) // // type RemediationLevelType string // // const ( // OfficialFix RemediationLevelType = "OFFICIAL_FIX" // RemediationLevelTypeNOTDEFINED RemediationLevelType = "NOT_DEFINED" // TemporaryFix RemediationLevelType = "TEMPORARY_FIX" // Unavailable RemediationLevelType = "UNAVAILABLE" // Workaround RemediationLevelType = "WORKAROUND" //) // // type ConfidenceType string // // const ( // ConfidenceTypeNOTDEFINED ConfidenceType = "NOT_DEFINED" // Confirmed ConfidenceType = "CONFIRMED" // Reasonable ConfidenceType = "REASONABLE" // Unknown ConfidenceType = "UNKNOWN" //) // // type ScopeType string // // const ( // ScopeTypeCHANGED ScopeType = "CHANGED" // ScopeTypeUNCHANGED ScopeType = "UNCHANGED" //) // // type UserInteractionType string // // const ( // UserInteractionTypeNONE UserInteractionType = "NONE" // UserInteractionTypeREQUIRED UserInteractionType = "REQUIRED" //) ================================================ FILE: grype/db/internal/provider/unmarshal/nvd/cvss40/cvss40.go ================================================ package cvss40 // note: this was autogenerated with some manual tweaking type Cvss40 struct { Version string `json:"version"` VectorString string `json:"vectorString"` BaseScore float64 `json:"baseScore"` BaseSeverity SeverityType `json:"baseSeverity"` // AttackVector string `json:"attackVector"` // AttackComplexity string `json:"attackComplexity"` // AttackRequirements string `json:"attackRequirements"` // PrivilegesRequired string `json:"privilegesRequired"` // UserInteraction string `json:"userInteraction"` // VulnConfidentialityImpact string `json:"vulnConfidentialityImpact"` // VulnIntegrityImpact string `json:"vulnIntegrityImpact"` // VulnAvailabilityImpact string `json:"vulnAvailabilityImpact"` // SubConfidentialityImpact string `json:"subConfidentialityImpact"` // SubIntegrityImpact string `json:"subIntegrityImpact"` // SubAvailabilityImpact string `json:"subAvailabilityImpact"` // ExploitMaturity string `json:"exploitMaturity"` // ConfidentialityRequirement string `json:"confidentialityRequirement"` // IntegrityRequirement string `json:"integrityRequirement"` // AvailabilityRequirement string `json:"availabilityRequirement"` // ModifiedAttackVector string `json:"modifiedAttackVector"` // ModifiedAttackComplexity string `json:"modifiedAttackComplexity"` // ModifiedAttackRequirements string `json:"modifiedAttackRequirements"` // ModifiedPrivilegesRequired string `json:"modifiedPrivilegesRequired"` // ModifiedUserInteraction string `json:"modifiedUserInteraction"` // ModifiedVulnConfidentialityImpact string `json:"modifiedVulnConfidentialityImpact"` // ModifiedVulnIntegrityImpact string `json:"modifiedVulnIntegrityImpact"` // ModifiedVulnAvailabilityImpact string `json:"modifiedVulnAvailabilityImpact"` // ModifiedSubConfidentialityImpact string `json:"modifiedSubConfidentialityImpact"` // ModifiedSubIntegrityImpact string `json:"modifiedSubIntegrityImpact"` // ModifiedSubAvailabilityImpact string `json:"modifiedSubAvailabilityImpact"` // Safety string `json:"Safety"` // Automatable string `json:"Automatable"` // Recovery string `json:"Recovery"` // ValueDensity string `json:"valueDensity"` // VulnerabilityResponseEffort string `json:"vulnerabilityResponseEffort"` // ProviderUrgency string `json:"providerUrgency"` } type SeverityType string const ( SeverityTypeCritical SeverityType = "CRITICAL" SeverityTypeHIGH SeverityType = "HIGH" SeverityTypeLOW SeverityType = "LOW" SeverityTypeMEDIUM SeverityType = "MEDIUM" SeverityTypeNONE SeverityType = "NONE" ) ================================================ FILE: grype/db/internal/provider/unmarshal/nvd_vulnerability.go ================================================ package unmarshal import ( "io" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" ) type ( NVDVulnerability = nvd.CveItem ) func NvdVulnerabilityEntries(reader io.Reader) ([]nvd.Vulnerability, error) { return unmarshalSingleOrMulti[nvd.Vulnerability](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/openvex_vulnerability.go ================================================ package unmarshal import ( "io" govex "github.com/openvex/go-vex/pkg/vex" ) type OpenVEXVulnerability = govex.Statement func OpenVEXVulnerabilityEntries(reader io.Reader) ([]OpenVEXVulnerability, error) { return unmarshalSingleOrMulti[OpenVEXVulnerability](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/os_vulnerability.go ================================================ package unmarshal import ( "fmt" "io" "sort" "strings" "unicode" "github.com/anchore/grype/grype/version" ) type OSFixedIn struct { Module *string `json:"Module,omitempty"` Name string `json:"Name"` NamespaceName string `json:"NamespaceName"` VendorAdvisory struct { AdvisorySummary []struct { ID string `json:"ID"` Link string `json:"Link"` } `json:"AdvisorySummary"` NoAdvisory bool `json:"NoAdvisory"` } `json:"VendorAdvisory"` Version string `json:"Version"` VersionFormat string `json:"VersionFormat"` VulnerableRange string `json:"VulnerableRange"` Available struct { Date string `json:"Date,omitempty"` Kind string `json:"Kind,omitempty"` } `json:"Available,omitempty"` } type OSFixedIns []OSFixedIn type OSVulnerability struct { Vulnerability struct { CVSS []struct { BaseMetrics struct { BaseScore float64 `json:"base_score"` BaseSeverity string `json:"base_severity"` ExploitabilityScore float64 `json:"exploitability_score"` ImpactScore float64 `json:"impact_score"` } `json:"base_metrics"` Status string `json:"status"` VectorString string `json:"vector_string"` Version string `json:"version"` } `json:"CVSS"` Description string `json:"Description"` FixedIn OSFixedIns Link string `json:"Link"` Metadata struct { Issued string `json:"Issued"` Updated string `json:"Updated"` RefID string `json:"RefId"` CVE []struct { Name string `json:"Name"` Link string `json:"Link"` } `json:"CVE"` NVD struct { CVSSv2 struct { Score float64 `json:"Score"` Vectors string `json:"Vectors"` } `json:"CVSSv2"` } `json:"NVD"` } `json:"Metadata"` Name string `json:"Name"` NamespaceName string `json:"NamespaceName"` Severity string `json:"Severity"` } `json:"Vulnerability"` } func (o OSVulnerability) IsEmpty() bool { return o.Vulnerability.Name == "" } func OSVulnerabilityEntries(reader io.Reader) ([]OSVulnerability, error) { return unmarshalSingleOrMulti[OSVulnerability](reader) } // FilterToHighestModularity returns a new distinct set of fixes, keeping only the highest version module fix. // In cases where there is no modularity the fix is kept. // func (fixes OSFixedIns) FilterToHighestModularity() OSFixedIns { if len(fixes) < 2 { return fixes } type moduleFix struct { constraint version.Constraint fix OSFixedIn } var keep []OSFixedIn moduleHighestFixes := make(map[string]moduleFix) for _, f := range fixes { validModule, moduleName, v, c := moduleNameAndVersion(f.Module) if !validModule { keep = append(keep, f) continue } k := fmt.Sprintf("%s|%s|%s", f.Name, f.NamespaceName, moduleName) if m, exists := moduleHighestFixes[k]; exists { satisfied, err := m.constraint.Satisfied(v) if err != nil { keep = append(keep, f) continue } if !satisfied { continue } } moduleHighestFixes[k] = moduleFix{ constraint: *c, fix: f, } } // To ensure stable output ordering for tests var orderedKeys []string for k := range moduleHighestFixes { orderedKeys = append(orderedKeys, k) } sort.Strings(orderedKeys) for _, k := range orderedKeys { keep = append(keep, moduleHighestFixes[k].fix) } return keep } func moduleNameAndVersion(module *string) (bool, string, *version.Version, *version.Constraint) { if module == nil || *module == "" { return false, "", nil, nil } moduleComponents := strings.Split(*module, ":") if len(moduleComponents) < 2 { return false, "", nil, nil } moduleName := strings.Join(moduleComponents[0:len(moduleComponents)-1], ":") moduleVersion := moduleComponents[len(moduleComponents)-1] isPotentiallyVersionedModule := len(moduleVersion) > 0 && unicode.IsDigit(rune(moduleVersion[0])) v, c := moduleVersionConstraint(moduleVersion) if v == nil || c == nil { return false, "", nil, nil } return isPotentiallyVersionedModule, moduleName, v, c } func moduleVersionConstraint(moduleVersion string) (*version.Version, *version.Constraint) { v := version.New(moduleVersion, version.UnknownFormat) if v == nil { return nil, nil } c := version.MustGetConstraint(fmt.Sprintf("> %s", moduleVersion), version.UnknownFormat) return v, &c } ================================================ FILE: grype/db/internal/provider/unmarshal/os_vulnerability_test.go ================================================ package unmarshal import ( "testing" "github.com/stretchr/testify/assert" ) func Test_OSFixedIns_FilterToHighestModularity(t *testing.T) { keepAll := []OSFixedIn{ { Module: nil, Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, } table := []struct { name string start OSFixedIns expect OSFixedIns }{ { name: "go case: no filtering", start: keepAll, expect: keepAll, }, { name: "keep the highest version of a module", start: []OSFixedIn{ { Module: func() *string { x := "name:1.0.2" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, expect: []OSFixedIn{ { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, }, { name: "keep the highest version of a module (version processing flipped)", start: []OSFixedIn{ { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.2" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, }, expect: []OSFixedIn{ { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, }, { name: "keep distinct module names (even though the package info is the same)", start: []OSFixedIn{ { Module: func() *string { x := "name-1:1.0.2" // <-- important return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, expect: []OSFixedIn{ { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, { Module: func() *string { x := "name-1:1.0.2" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, }, }, { name: "keep distinct namespaces", start: []OSFixedIn{ { Module: func() *string { x := "name:1.0.2" return &x }(), Name: "name", NamespaceName: "namespace-1", // <-- important Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, expect: []OSFixedIn{ { Module: func() *string { x := "name:1.0.2" return &x }(), Name: "name", NamespaceName: "namespace-1", Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, }, { name: "keep module with no numeric versions", start: []OSFixedIn{ { Module: func() *string { x := "name:prefix1.0.2" // <-- important return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, expect: []OSFixedIn{ { Module: func() *string { x := "name:prefix1.0.2" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.2", VersionFormat: "semver", }, { Module: func() *string { x := "name:1.0.3" return &x }(), Name: "name", NamespaceName: "namespace", Version: "v1.0.3", VersionFormat: "semver", }, }, }, } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { got := tt.start.FilterToHighestModularity() assert.Equal(t, tt.expect, got) }) } } ================================================ FILE: grype/db/internal/provider/unmarshal/osv_vulnerability.go ================================================ package unmarshal import ( "io" "github.com/google/osv-scanner/pkg/models" ) type OSVVulnerability = models.Vulnerability func OSVVulnerabilityEntries(reader io.Reader) ([]OSVVulnerability, error) { return unmarshalSingleOrMulti[OSVVulnerability](reader) } ================================================ FILE: grype/db/internal/provider/unmarshal/single_or_multi.go ================================================ package unmarshal import ( "bytes" "encoding/json" "fmt" "io" ) func unmarshalSingleOrMulti[T interface{}](reader io.Reader) ([]T, error) { var entry T var buf bytes.Buffer r := io.TeeReader(reader, &buf) dec := json.NewDecoder(r) err := dec.Decode(&entry) if err == nil { return []T{entry}, nil } // TODO: enhance the error handling to return the original error if the item is found to not be an array of items var entries []T dec = json.NewDecoder(io.MultiReader(&buf, reader)) if err = dec.Decode(&entries); err != nil { return nil, fmt.Errorf("unable to decode vulnerability: %w", handleJSONUnmarshalError(err)) } return entries, nil } ================================================ FILE: grype/db/internal/sqlite/nullable_types.go ================================================ package sqlite import ( "database/sql" "encoding/json" ) type NullString struct { sql.NullString } func NewNullString(s string, valid bool) NullString { return NullString{ sql.NullString{ String: s, Valid: valid, }, } } func ToNullString(v any) NullString { nullString := NullString{} nullString.Valid = false if v != nil { var stringValue string if s, ok := v.(string); ok { stringValue = s } else { vBytes, err := json.Marshal(v) if err != nil { // TODO: just no panic(err) } stringValue = string(vBytes) } if stringValue != "null" { nullString.String = stringValue nullString.Valid = true } } return nullString } func (v NullString) ToByteSlice() []byte { if v.Valid { return []byte(v.String) } return []byte("null") } func (v NullString) MarshalJSON() ([]byte, error) { if v.Valid { return json.Marshal(v.String) } return json.Marshal(nil) } func (v *NullString) UnmarshalJSON(data []byte) error { if data != nil && string(data) != "null" { v.Valid = true v.String = string(data) } else { v.Valid = false } return nil } ================================================ FILE: grype/db/internal/sqlite/nullable_types_test.go ================================================ package sqlite import ( "testing" "github.com/stretchr/testify/assert" ) func TestToNullString(t *testing.T) { tests := []struct { name string input any expected NullString }{ { name: "Nil input", input: nil, expected: NullString{}, }, { name: "String null", input: "null", expected: NullString{}, }, { name: "Other string", input: "Hello there {}", expected: NewNullString("Hello there {}", true), }, { name: "Single struct with all fields populated", input: struct { Boolean bool `json:"boolean"` String string `json:"string"` Integer int `json:"integer"` InnerStruct struct { StringList []string `json:"string_list"` } `json:"inner_struct"` }{ Boolean: true, String: "{}", Integer: 1034, InnerStruct: struct { StringList []string `json:"string_list"` }{ StringList: []string{"a", "b", "c"}, }, }, expected: NewNullString(`{"boolean":true,"string":"{}","integer":1034,"inner_struct":{"string_list":["a","b","c"]}}`, true), }, { name: "Single struct with one field populated", input: struct { Boolean bool `json:"boolean"` String string `json:"string"` Integer int `json:"integer"` InnerStruct struct { StringList []string `json:"string_list"` } `json:"inner_struct"` }{ Boolean: true, }, expected: NewNullString(`{"boolean":true,"string":"","integer":0,"inner_struct":{"string_list":null}}`, true), }, { name: "Single struct with one field populated omit empty", input: struct { Boolean bool `json:"boolean,omitempty"` String string `json:"string,omitempty"` Integer int `json:"integer,omitempty"` InnerStruct struct { StringList []string `json:"string_list,omitempty"` } `json:"inner_struct,omitempty"` }{ Boolean: true, }, expected: NewNullString(`{"boolean":true,"inner_struct":{}}`, true), }, { name: "Array of structs", input: []struct { Boolean bool `json:"boolean,omitempty"` String string `json:"string,omitempty"` Integer int `json:"integer,omitempty"` }{ { Boolean: true, String: "{}", Integer: 1034, }, { String: "[{}]", }, { Integer: -5000, Boolean: false, }, }, expected: NewNullString(`[{"boolean":true,"string":"{}","integer":1034},{"string":"[{}]"},{"integer":-5000}]`, true), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result := ToNullString(test.input) assert.Equal(t, test.expected, result) }) } } ================================================ FILE: grype/db/internal/tarutil/file_entry.go ================================================ package tarutil import ( "fmt" "io" "os" ) var _ Entry = (*FileEntry)(nil) type FileEntry struct { Path string } func NewEntryFromFilePath(path string) Entry { return FileEntry{ Path: path, } } func NewEntryFromFilePaths(paths ...string) []Entry { var entries []Entry for _, path := range paths { entries = append(entries, NewEntryFromFilePath(path)) } return entries } func (t FileEntry) writeEntry(tw lowLevelWriter) error { fi, err := os.Lstat(t.Path) if err != nil { return fmt.Errorf("unable to stat file %q: %w", t.Path, err) } return writeEntry(tw, t.Path, fi, func() (io.Reader, error) { return os.Open(t.Path) }) } ================================================ FILE: grype/db/internal/tarutil/file_entry_test.go ================================================ package tarutil import ( "archive/tar" "bytes" "os" "path/filepath" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var _ lowLevelWriter = (*mockTarWriter)(nil) type mockTarWriter struct { headers []*tar.Header buffers []*bytes.Buffer closeCalled bool flushCalled bool closeErr error flushErr error } func (m *mockTarWriter) Flush() error { m.flushCalled = true return m.flushErr } func (m *mockTarWriter) Close() error { m.closeCalled = true return m.closeErr } func (m *mockTarWriter) WriteHeader(header *tar.Header) error { m.headers = append(m.headers, header) m.buffers = append(m.buffers, &bytes.Buffer{}) return nil } func (m *mockTarWriter) Write(b []byte) (int, error) { return m.buffers[len(m.buffers)-1].Write(b) } func TestFileEntry_writeEntry(t *testing.T) { testStr := "hello world" tests := []struct { name string file func(t *testing.T) string wantErr require.ErrorAssertionFunc }{ { name: "valid file", file: func(t *testing.T) string { dir := t.TempDir() dest := filepath.Join(dir, "file.txt") require.NoError(t, os.WriteFile(dest, []byte(testStr), 0644)) return dest }, }, { name: "invalid file", file: func(t *testing.T) string { return filepath.Join("/tmp/invalid/path", uuid.New().String()) }, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } expectedName := tt.file(t) fe := NewEntryFromFilePath(expectedName) tw := &mockTarWriter{} err := fe.writeEntry(tw) tt.wantErr(t, err) if err != nil { return } assert.NoError(t, err) require.Len(t, tw.headers, 1) assert.Equal(t, expectedName, tw.headers[0].Name) assert.Equal(t, int64(len(testStr)), tw.headers[0].Size) assert.Equal(t, testStr, tw.buffers[0].String()) }) } } ================================================ FILE: grype/db/internal/tarutil/populate.go ================================================ package tarutil // PopulateWithPaths creates a compressed tar from the given paths. func PopulateWithPaths(tarPath string, filePaths ...string) error { return PopulateWithPathsAndCompressors(tarPath, nil, filePaths...) } // PopulateWithPathsAndCompressors creates a compressed tar from the given paths using custom compressor commands. func PopulateWithPathsAndCompressors(tarPath string, compressorCommands map[string]string, filePaths ...string) error { w, err := NewWriterWithCompressors(tarPath, compressorCommands) if err != nil { return err } defer w.Close() for _, entry := range NewEntryFromFilePaths(filePaths...) { if err := w.WriteEntry(entry); err != nil { return err } } return nil } ================================================ FILE: grype/db/internal/tarutil/populate_test.go ================================================ package tarutil import ( "archive/tar" "compress/gzip" "io" "os" "path/filepath" "strings" "testing" "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPopulateWithPaths(t *testing.T) { tests := []struct { name string tarPath string wantErr bool }{ { name: "plain tar", tarPath: "foo.tar", wantErr: false, }, { name: "tar gz", tarPath: "foo.tar.gz", wantErr: false, }, { name: "tar zst", tarPath: "foo.tar.zst", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() tempPath := filepath.Join(dir, "some-path.txt") f, err := os.Create(tempPath) require.NoError(t, err) _, err = f.Write([]byte("hello world\n")) require.NoError(t, err) archivePath := filepath.Join(dir, tt.tarPath) err = PopulateWithPaths(archivePath, tempPath) require.NoError(t, err) var r io.Reader f, err = os.Open(archivePath) require.NoError(t, err) defer f.Close() switch { case strings.HasSuffix(archivePath, ".tar.gz"): r, err = gzip.NewReader(f) require.NoError(t, err) case strings.HasSuffix(archivePath, ".tar.zst"): r, err = zstd.NewReader(f) require.NoError(t, err) case strings.HasSuffix(archivePath, ".tar"): r = f default: t.Fatalf("unsupported archive type: %s", archivePath) } tr := tar.NewReader(r) h, err := tr.Next() require.NoError(t, err) assert.Equal(t, h.Name, tempPath) b, err := io.ReadAll(tr) assert.Equal(t, []byte("hello world\n"), b) }) } } ================================================ FILE: grype/db/internal/tarutil/reader_entry.go ================================================ package tarutil import ( "archive/tar" "bytes" "fmt" "io" "os" "github.com/anchore/grype/internal/log" ) var _ Entry = (*ReaderEntry)(nil) type ReaderEntry struct { Reader io.Reader Filename string FileInfo os.FileInfo } func NewEntryFromBytes(by []byte, filename string, fileInfo os.FileInfo) Entry { return ReaderEntry{ Reader: bytes.NewReader(by), Filename: filename, FileInfo: fileInfo, } } func (t ReaderEntry) writeEntry(tw lowLevelWriter) error { log.WithFields("path", t.Filename).Trace("adding stream to archive") return writeEntry(tw, t.Filename, t.FileInfo, func() (io.Reader, error) { return t.Reader, nil }) } // autoDeleteFile wraps an *os.File and deletes it when closed. type autoDeleteFile struct { *os.File } func (f *autoDeleteFile) Close() error { name := f.Name() err := f.File.Close() if removeErr := os.Remove(name); removeErr != nil && err == nil { err = removeErr } return err } // readerWithSize determines the size of the reader's content without reading the entire content into memory. // For known reader types (bytes.Reader, os.File), it queries the size directly. // For unknown types, it copies to a temp file to avoid loading into memory. // Returns the size, a ReadCloser for the content (may be different from input), and any error. func readerWithSize(reader io.Reader) (int64, io.ReadCloser, error) { switch r := reader.(type) { case *bytes.Reader: // For bytes.Reader (used by NewEntryFromBytes), get actual size return r.Size(), io.NopCloser(reader), nil case interface{ Stat() (os.FileInfo, error) }: // For *os.File, use Stat to get size stat, err := r.Stat() if err != nil { return 0, nil, err } // Check if it's already a ReadCloser if rc, ok := reader.(io.ReadCloser); ok { return stat.Size(), rc, nil } return 0, nil, fmt.Errorf("reader with Stat() must implement io.ReadCloser") default: // Fallback for unknown reader types: copy to temp file to avoid loading into memory tmpFile, err := os.CreateTemp("", "grype-db-tar-*") if err != nil { return 0, nil, fmt.Errorf("unable to create temp file: %w", err) } size, err := io.Copy(tmpFile, reader) if err != nil { tmpFile.Close() os.Remove(tmpFile.Name()) return 0, nil, fmt.Errorf("unable to copy to temp file: %w", err) } // Seek back to beginning for reading if _, err := tmpFile.Seek(0, 0); err != nil { tmpFile.Close() os.Remove(tmpFile.Name()) return 0, nil, fmt.Errorf("unable to seek temp file: %w", err) } return size, &autoDeleteFile{File: tmpFile}, nil } } func writeEntry(tw lowLevelWriter, filename string, fileInfo os.FileInfo, opener func() (io.Reader, error)) error { log.WithFields("path", filename).Trace("adding file to archive") header, err := tar.FileInfoHeader(fileInfo, "") if err != nil { return err } header.Name = filename switch fileInfo.Mode() & os.ModeType { case os.ModeDir: header.Size = 0 err = tw.WriteHeader(header) if err != nil { return err } return nil case os.ModeSymlink: linkTarget, err := os.Readlink(filename) if err != nil { return err } header.Linkname = linkTarget header.Size = 0 err = tw.WriteHeader(header) if err != nil { return err } return nil default: reader, err := opener() if err != nil { return err } size, readCloser, err := readerWithSize(reader) if err != nil { return err } defer readCloser.Close() header.Size = size if err := tw.WriteHeader(header); err != nil { return err } // Stream the file contents directly to the tar writer if _, err := io.Copy(tw, readCloser); err != nil { return err } // ensure proper alignment in the tar archive (padding with zeros) if err := tw.Flush(); err != nil { return err } } return nil } ================================================ FILE: grype/db/internal/tarutil/reader_entry_test.go ================================================ package tarutil import ( "archive/tar" "bytes" "io" "io/fs" "os" "path/filepath" "strings" "testing" "time" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var _ lowLevelWriter = (*mockTarWriter)(nil) var _ os.FileInfo = (*mockFileInfo)(nil) type mockFileInfo struct { name string size int64 mode fs.FileMode modTime time.Time isDir bool sys any } func (m mockFileInfo) Name() string { return m.name } func (m mockFileInfo) Size() int64 { return m.size } func (m mockFileInfo) Mode() fs.FileMode { return m.mode } func (m mockFileInfo) ModTime() time.Time { return m.modTime } func (m mockFileInfo) IsDir() bool { return m.isDir } func (m mockFileInfo) Sys() any { return m.sys } func TestReaderEntry_writeEntry(t *testing.T) { d := t.TempDir() file := filepath.Join(d, "file.txt") require.NoError(t, os.WriteFile(file, []byte("hello world"), 0644)) link := filepath.Join(d, "link") require.NoError(t, os.Symlink(file, link)) dir := filepath.Join(d, "dir") require.NoError(t, os.Mkdir(dir, 0755)) tests := []struct { name string typeFlag byte bytes []byte filename string fileinfo os.FileInfo wantErr require.ErrorAssertionFunc expectFlush bool fs afero.Fs }{ { name: "valid file", typeFlag: tar.TypeReg, bytes: []byte("hello world"), filename: file, expectFlush: true, fileinfo: &mockFileInfo{ name: file, size: 11, mode: 0644, modTime: time.Now(), isDir: false, sys: nil, }, }, { name: "symlink", typeFlag: tar.TypeSymlink, bytes: nil, filename: link, expectFlush: false, fileinfo: &mockFileInfo{ name: link, size: 0, mode: os.ModeSymlink, modTime: time.Now(), isDir: false, }, }, { name: "directory", typeFlag: tar.TypeDir, bytes: nil, filename: dir, expectFlush: false, fileinfo: &mockFileInfo{ name: dir, size: 0, mode: os.ModeDir, modTime: time.Now(), isDir: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } fe := NewEntryFromBytes(tt.bytes, tt.filename, tt.fileinfo) tw := &mockTarWriter{} err := fe.writeEntry(tw) tt.wantErr(t, err) if err != nil { return } assert.NoError(t, err) require.Len(t, tw.headers, 1) assert.Equal(t, tt.typeFlag, tw.headers[0].Typeflag) assert.Equal(t, tt.filename, tw.headers[0].Name) assert.Equal(t, int64(len(tt.bytes)), tw.headers[0].Size) assert.Equal(t, string(tt.bytes), tw.buffers[0].String()) assert.Equal(t, tt.expectFlush, tw.flushCalled) }) } } func Test_readerWithSize(t *testing.T) { testData := "hello world from test" tests := []struct { name string reader func(t *testing.T) io.Reader wantSize int64 wantErr require.ErrorAssertionFunc }{ { name: "bytes.Reader", reader: func(t *testing.T) io.Reader { return bytes.NewReader([]byte(testData)) }, wantSize: int64(len(testData)), }, { name: "os.File success", reader: func(t *testing.T) io.Reader { dir := t.TempDir() path := filepath.Join(dir, "test.txt") require.NoError(t, os.WriteFile(path, []byte(testData), 0644)) f, err := os.Open(path) require.NoError(t, err) t.Cleanup(func() { f.Close() }) return f }, wantSize: int64(len(testData)), }, { name: "os.File stat fails", reader: func(t *testing.T) io.Reader { dir := t.TempDir() path := filepath.Join(dir, "test.txt") require.NoError(t, os.WriteFile(path, []byte(testData), 0644)) f, err := os.Open(path) require.NoError(t, err) f.Close() return f }, wantErr: require.Error, }, { name: "unknown reader creates temp file", reader: func(t *testing.T) io.Reader { return strings.NewReader(testData) }, wantSize: int64(len(testData)), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } reader := tt.reader(t) size, rc, err := readerWithSize(reader) tt.wantErr(t, err) if err != nil { return } defer rc.Close() assert.Equal(t, tt.wantSize, size) content, err := io.ReadAll(rc) require.NoError(t, err) assert.Equal(t, testData, string(content)) }) } } func Test_autoDeleteFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.txt") require.NoError(t, os.WriteFile(path, []byte("test content"), 0644)) f, err := os.Open(path) require.NoError(t, err) adf := &autoDeleteFile{File: f} _, err = os.Stat(path) require.NoError(t, err) err = adf.Close() require.NoError(t, err) _, err = os.Stat(path) assert.True(t, os.IsNotExist(err)) } ================================================ FILE: grype/db/internal/tarutil/tar.go ================================================ package tarutil import ( "archive/tar" "io" ) // Writer represents a facade for writing entries to a tar file. type Writer interface { WriteEntry(Entry) error io.Closer } // lowLevelWriter abstracts the *tar.Writer from the standard library. type lowLevelWriter interface { WriteHeader(*tar.Header) error Flush() error io.WriteCloser } // Entry represents an entry that can be written to a tar file via a tar.Writer from the standard library. type Entry interface { writeEntry(writer lowLevelWriter) error } ================================================ FILE: grype/db/internal/tarutil/writer.go ================================================ package tarutil import ( "archive/tar" "bufio" "compress/gzip" "fmt" "io" "os" "os/exec" "strings" "github.com/google/shlex" "github.com/klauspost/compress/flate" "github.com/anchore/grype/internal/log" ) var ErrUnsupportedArchiveSuffix = fmt.Errorf("archive name has an unsupported suffix") var _ Writer = (*writer)(nil) type writer struct { compressor io.WriteCloser writer *tar.Writer } // NewWriter creates a new tar writer that writes to the specified archive path. Supports .tar.gz, .tar.zst, .tar.xz, and .tar file extensions. func NewWriter(archivePath string) (Writer, error) { return NewWriterWithCompressors(archivePath, nil) } // NewWriterWithCompressors creates a new tar writer with custom compressor commands. If compressorCommands is nil or empty, it uses default commands. func NewWriterWithCompressors(archivePath string, compressorCommands map[string]string) (Writer, error) { w, err := newCompressorWithCommands(archivePath, compressorCommands) if err != nil { return nil, err } tw := tar.NewWriter(w) return &writer{ compressor: w, writer: tw, }, nil } func newCompressorWithCommands(archivePath string, compressorCommands map[string]string) (io.WriteCloser, error) { archive, err := os.Create(archivePath) if err != nil { return nil, err } // check for custom compressor command first for ext, cmd := range compressorCommands { if strings.HasSuffix(archivePath, "."+ext) { log.Debugf("using custom compressor command for %s: %s", ext, cmd) return newShellCompressor(cmd, archive) } } log.Debugf("no custom compressor command found for %s, using default", archivePath) // fall back to default compressor commands switch { case strings.HasSuffix(archivePath, ".tar.gz"): return gzip.NewWriterLevel(archive, flate.BestCompression) case strings.HasSuffix(archivePath, ".tar.zst"): // note: since we're using --ultra this tends to have a high memory usage at decompression time // For ~700 MB payload that is compressing down to ~60 MB, that would need ~130 MB of memory (--ultra -22) // for the same payload compressing down to ~65MB, that would need ~70MB of memory (--ultra -21) return newShellCompressor("zstd -T0 -22 --ultra -c -vv", archive) case strings.HasSuffix(archivePath, ".tar.xz"): return newShellCompressor("xz -9 --threads=0 -c -vv", archive) case strings.HasSuffix(archivePath, ".tar"): return archive, nil } return nil, ErrUnsupportedArchiveSuffix } // shellCompressor wraps the stdin pipe of an external compression process and ensures proper cleanup. type shellCompressor struct { cmd *exec.Cmd pipe io.WriteCloser } func newShellCompressor(c string, archive io.Writer) (*shellCompressor, error) { args, err := shlex.Split(c) if err != nil { return nil, fmt.Errorf("unable to parse command: %w", err) } binary := args[0] binPath, err := exec.LookPath(binary) if err != nil { return nil, fmt.Errorf("unable to find binary %q: %w", binary, err) } if binPath == "" { return nil, fmt.Errorf("unable to find binary %q in PATH", binary) } args = args[1:] cmd := exec.Command(binary, args...) log.Debug(strings.Join(cmd.Args, " ")) cmd.Stdout = archive stderrPipe, err := cmd.StderrPipe() if err != nil { return nil, fmt.Errorf("unable to create stderr pipe: %w", err) } pipe, err := cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("unable to create stdin pipe: %w", err) } if err := cmd.Start(); err != nil { return nil, fmt.Errorf("unable to start process: %w", err) } go func() { scanner := bufio.NewScanner(stderrPipe) for scanner.Scan() { log.Debugf("[%s] %s", binary, scanner.Text()) } if err := scanner.Err(); err != nil { log.Errorf("[%s] error reading stderr: %v", binary, err) } }() return &shellCompressor{ cmd: cmd, pipe: pipe, }, nil } func (sc *shellCompressor) Write(p []byte) (int, error) { return sc.pipe.Write(p) } func (sc *shellCompressor) Close() error { if err := sc.pipe.Close(); err != nil { return fmt.Errorf("unable to close compression stdin pipe: %w", err) } if err := sc.cmd.Wait(); err != nil { return fmt.Errorf("compression process error: %w", err) } return nil } func (w *writer) WriteEntry(entry Entry) error { return entry.writeEntry(w.writer) } func (w *writer) Close() error { if w.writer != nil { err := w.writer.Close() w.writer = nil if err != nil { return fmt.Errorf("unable to close tar writer: %w", err) } } if w.compressor != nil { err := w.compressor.Close() w.compressor = nil return err } return nil } ================================================ FILE: grype/db/internal/tarutil/writer_test.go ================================================ package tarutil import ( "archive/tar" "compress/gzip" "io" "os" "path/filepath" "strings" "testing" "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewWriter(t *testing.T) { tests := []struct { name string archivePath string wantErr bool }{ { name: "tar.gz compressor", archivePath: "test.tar.gz", wantErr: false, }, { name: "tar.zst compressor", archivePath: "test.tar.zst", wantErr: false, }, { name: "tar compressor", archivePath: "test.tar", wantErr: false, }, { name: "unsupported compressor", archivePath: "test.txt", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() require.NoError(t, os.Chdir(dir)) testFilePath := "testfile" testString := "hello world" require.NoError(t, os.WriteFile(testFilePath, []byte(testString), 0644)) archivePath := filepath.Join(dir, tt.archivePath) w, err := NewWriter(archivePath) if tt.wantErr { require.Error(t, err) return } else { require.NoError(t, err) } entry := NewEntryFromFilePath(testFilePath) err = w.WriteEntry(entry) require.NoError(t, err) err = w.Close() require.NoError(t, err) _, err = os.Stat(archivePath) assert.NoError(t, err) var r io.Reader f, err := os.Open(archivePath) require.NoError(t, err) defer f.Close() switch { case strings.HasSuffix(archivePath, ".tar.gz"): r, err = gzip.NewReader(f) require.NoError(t, err) case strings.HasSuffix(archivePath, ".tar.zst"): r, err = zstd.NewReader(f) require.NoError(t, err) case strings.HasSuffix(archivePath, ".tar"): r = f default: t.Fatalf("unsupported archive type: %s", archivePath) } tr := tar.NewReader(r) hdr, err := tr.Next() require.NoError(t, err) assert.Equal(t, testFilePath, hdr.Name) assert.Equal(t, int64(len(testString)), hdr.Size) content, err := io.ReadAll(tr) require.NoError(t, err) assert.Equal(t, testString, string(content)) }) } } ================================================ FILE: grype/db/internal/testutil/utils.go ================================================ package testutil import ( "log" "os" ) func CloseFile(f *os.File) { err := f.Close() if err != nil { log.Fatal("error closing file") } } ================================================ FILE: grype/db/internal/versionutil/clean_fixed_in_version.go ================================================ package versionutil import "strings" func CleanFixedInVersion(version string) string { switch strings.TrimSpace(strings.ToLower(version)) { case "none", "": return "" default: return version } } ================================================ FILE: grype/db/internal/versionutil/constraint.go ================================================ package versionutil import ( "regexp" "strings" ) // match examples: // >= 5.0.0 // <= 6.1.2.beta // >= 5.0.0 // < 6.1 // > 5.0.0 // >=5 // <6 var forceSemVerPattern = regexp.MustCompile(`[><=]+\s*[^<>=]+`) func EnforceSemVerConstraint(constraint string) string { constraint = CleanConstraint(constraint) if constraint == "" { return "" } return strings.ReplaceAll(strings.Join(forceSemVerPattern.FindAllString(constraint, -1), ", "), " ", "") } func AndConstraints(c ...string) string { return strings.Join(c, " ") } func OrConstraints(c ...string) string { return strings.Join(c, " || ") } func CleanConstraint(constraint string) string { if strings.ToLower(constraint) == "none" { return "" } return constraint } ================================================ FILE: grype/db/internal/versionutil/constraint_test.go ================================================ package versionutil import "testing" func TestEnforceSemVerConstraint(t *testing.T) { tests := []struct { value string expected string }{ { value: " >= 5.0.0<7.1 ", expected: ">=5.0.0,<7.1", }, { value: "None", expected: "", }, { value: "", expected: "", }, } for _, test := range tests { t.Run(test.value, func(t *testing.T) { actual := EnforceSemVerConstraint(test.value) if actual != test.expected { t.Errorf("mismatch: '%s'!='%s'", actual, test.expected) } }) } } ================================================ FILE: grype/db/package.go ================================================ package db import ( "os" "path/filepath" grypeDBLegacyDistribution "github.com/anchore/grype/grype/db/v5/distribution" v6process "github.com/anchore/grype/grype/db/v6/build" ) func Package(dbDir, publishBaseURL, overrideArchiveExtension string, compressorCommands map[string]string) error { // check if metadata file exists, if so, then this if _, err := os.Stat(filepath.Join(dbDir, grypeDBLegacyDistribution.MetadataFileName)); os.IsNotExist(err) { // TODO: detect from disk which version of the DB is present return v6process.CreateArchive(dbDir, overrideArchiveExtension, compressorCommands) } return packageLegacyDB(dbDir, publishBaseURL, overrideArchiveExtension, compressorCommands) } ================================================ FILE: grype/db/package_legacy.go ================================================ package db import ( "fmt" "net/url" "os" "path" "path/filepath" "strings" "time" "github.com/scylladb/go-set/strset" "github.com/spf13/afero" "github.com/anchore/grype/grype/db/internal/tarutil" grypeDBLegacy "github.com/anchore/grype/grype/db/v5" grypeDBLegacyDistribution "github.com/anchore/grype/grype/db/v5/distribution" grypeDBLegacyStore "github.com/anchore/grype/grype/db/v5/store" "github.com/anchore/grype/internal/log" ) // listingFiles is a set of files that should not be included in the archive var listingFiles = strset.New("listing.json", "latest.json", "history.json") func packageLegacyDB(dbDir, publishBaseURL, overrideArchiveExtension string, compressorCommands map[string]string) error { //nolint:funlen log.WithFields("from", dbDir, "url", publishBaseURL, "extension-override", overrideArchiveExtension).Info("packaging database") fs := afero.NewOsFs() metadata, err := grypeDBLegacyDistribution.NewMetadataFromDir(fs, dbDir) if err != nil { return err } if metadata == nil { return fmt.Errorf("no metadata found in %q", dbDir) } s, err := grypeDBLegacyStore.New(filepath.Join(dbDir, grypeDBLegacy.VulnerabilityStoreFileName), false) if err != nil { return fmt.Errorf("unable to open vulnerability store: %w", err) } id, err := s.GetID() if err != nil { return fmt.Errorf("unable to get vulnerability store ID: %w", err) } if id.SchemaVersion != metadata.Version { return fmt.Errorf("metadata version %d does not match vulnerability store version %d", metadata.Version, id.SchemaVersion) } u, err := url.Parse(publishBaseURL) if err != nil { return err } // we need a well-ordered string to append to the archive name to ensure uniqueness (to avoid overwriting // existing archives in the CDN) as well as to ensure that multiple archives created in the same day are // put in the correct order in the listing file. The DB timestamp represents the age of the data in the DB // not when the DB was created. The trailer represents the time the DB was packaged. trailer := fmt.Sprintf("%d", secondsSinceEpoch()) var extension = "tar.gz" if overrideArchiveExtension != "" { extension = strings.TrimLeft(overrideArchiveExtension, ".") } var found bool for _, valid := range []string{"tar.zst", "tar.gz"} { if valid == extension { found = true break } } if !found { return fmt.Errorf("invalid archive extension %q", extension) } // we attach a random value at the end of the file name to prevent from overwriting DBs in S3 that are already // cached in the CDN. Ideally this would be based off of the archive checksum but a random string is simpler. tarName := fmt.Sprintf( "vulnerability-db_v%d_%s_%s.%s", metadata.Version, metadata.Built.Format(time.RFC3339), trailer, extension, ) tarPath := path.Join(dbDir, tarName) if err := populateLegacyTar(tarPath, compressorCommands); err != nil { return err } log.WithFields("path", tarPath).Info("created database archive") entry, err := grypeDBLegacyDistribution.NewListingEntryFromArchive(fs, *metadata, tarPath, u) if err != nil { return fmt.Errorf("unable to create listing entry from archive: %w", err) } listing := grypeDBLegacyDistribution.NewListing(entry) listingPath := path.Join(dbDir, grypeDBLegacyDistribution.ListingFileName) if err = listing.Write(listingPath); err != nil { return err } log.WithFields("path", listingPath).Debug("created initial listing file") return nil } func populateLegacyTar(tarPath string, compressorCommands map[string]string) error { originalDir, err := os.Getwd() if err != nil { return fmt.Errorf("unable to get CWD: %w", err) } dbDir, tarName := filepath.Split(tarPath) if dbDir != "" { if err = os.Chdir(dbDir); err != nil { return fmt.Errorf("unable to cd to build dir: %w", err) } defer func() { if err = os.Chdir(originalDir); err != nil { log.Errorf("unable to cd to original dir: %v", err) } }() } fileInfos, err := os.ReadDir("./") if err != nil { return fmt.Errorf("unable to list db directory: %w", err) } var files []string for _, fi := range fileInfos { if !listingFiles.Has(fi.Name()) && !strings.Contains(fi.Name(), ".tar.") { files = append(files, fi.Name()) } } if err = tarutil.PopulateWithPathsAndCompressors(tarName, compressorCommands, files...); err != nil { return fmt.Errorf("unable to create db archive: %w", err) } return nil } func secondsSinceEpoch() int64 { return time.Now().UTC().Unix() } ================================================ FILE: grype/db/processors/annotated_openvex_processor.go ================================================ package processors // nolint:dupl import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type annotatedOpenVEXProcessor struct { transformer data.AnnotatedOpenVEXTransformerV2 } func NewV2AnnotatedOpenVEXProcessor(transformer data.AnnotatedOpenVEXTransformerV2) data.Processor { return &annotatedOpenVEXProcessor{ transformer: transformer, } } func (p annotatedOpenVEXProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.AnnotatedOpenVEXVulnerabilityEntries(reader) if err != nil { return nil, err } for _, entry := range entries { transformedEntries, err := p.transformer(entry, state) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p annotatedOpenVEXProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "annotated-openvex") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse annotated OpenVEX schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/eol_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type eolProcessor struct { transformer data.EOLTransformerV2 } func NewV2EOLProcessor(transformer data.EOLTransformerV2) data.Processor { return &eolProcessor{ transformer: transformer, } } func (p eolProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.EndOfLifeDateReleaseEntries(reader) if err != nil { return nil, err } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty EOL entry") continue } transformedEntries, err := p.transformer(entry, state) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p eolProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "eol") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse EOL schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/eol_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" ) func mockEOLProcessorTransform(entry unmarshal.EndOfLifeDateRelease, state provider.State) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: entry, }, }, nil } func TestEOLProcessor_Process(t *testing.T) { f, err := os.Open("testdata/eol.json") require.NoError(t, err) defer f.Close() processor := NewV2EOLProcessor(mockEOLProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "eol", }) assert.NoError(t, err) assert.Len(t, entries, 2) // Verify first entry is ubuntu entry0, ok := entries[0].Data.(unmarshal.EndOfLifeDateRelease) require.True(t, ok) assert.Equal(t, "ubuntu", entry0.Product) assert.Equal(t, "22.04", entry0.Name) assert.True(t, entry0.IsLTS) // Verify second entry is debian entry1, ok := entries[1].Data.(unmarshal.EndOfLifeDateRelease) require.True(t, ok) assert.Equal(t, "debian", entry1.Product) assert.Equal(t, "11", entry1.Name) } func TestEOLProcessor_Process_EmptyEntry(t *testing.T) { // Test that empty entries (product == "") are filtered out f, err := os.Open("testdata/eol-with-empty.json") require.NoError(t, err) defer f.Close() processor := NewV2EOLProcessor(mockEOLProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "eol", }) assert.NoError(t, err) // Should only have 1 entry (empty one filtered out) assert.Len(t, entries, 1) entry, ok := entries[0].Data.(unmarshal.EndOfLifeDateRelease) require.True(t, ok) assert.Equal(t, "alpine", entry.Product) } func TestEOLProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/eol/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.2.3", schemaURL: "https://example.com/vunnel/path/eol/schema-1.2.3.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/eol/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/eol/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, { name: "invalid schema segment", schemaURL: "https://example.com/vunnel/path/not-eol/schema-1.0.0.json", expected: false, }, { name: "epss schema should not match", schemaURL: "https://example.com/vunnel/path/epss/schema-1.0.0.json", expected: false, }, } p := eolProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/epss_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type epssProcessor struct { transformer data.EPSSTransformerV2 } func NewV2EPSSProcessor(transformer data.EPSSTransformerV2) data.Processor { return &epssProcessor{ transformer: transformer, } } func (p epssProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.EPSSEntries(reader) if err != nil { return nil, err } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty EPSS entry") continue } transformedEntries, err := p.transformer(entry, state) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p epssProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "epss") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse EPSS schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/epss_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" ) func mockEPSSProcessorTransform(entry unmarshal.EPSS, state provider.State) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: entry, }, }, nil } func TestEPSSProcessor_Process(t *testing.T) { f, err := os.Open("testdata/epss.json") require.NoError(t, err) defer f.Close() processor := NewV2EPSSProcessor(mockEPSSProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "epss", }) assert.NoError(t, err) assert.Len(t, entries, 2) } func TestEPSSProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/vulnerability/epss/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.2.3", schemaURL: "https://example.com/vunnel/path/vulnerability/epss/schema-1.2.3.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/vulnerability/epss/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/vulnerability/epss/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, { name: "invalid schema segment", schemaURL: "https://example.com/vunnel/path/vulnerability/not-epss/schema-1.0.0.json", expected: false, }, } p := epssProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/github_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type githubProcessor struct { transformer any } func NewGitHubProcessor(transformer data.GitHubTransformer) data.Processor { return &githubProcessor{ transformer: transformer, } } func NewV2GitHubProcessor(transformer data.GitHubTransformerV2) data.Processor { return &githubProcessor{ transformer: transformer, } } func (p githubProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.GitHubAdvisoryEntries(reader) if err != nil { return nil, err } var handle func(entry unmarshal.GitHubAdvisory) ([]data.Entry, error) switch t := p.transformer.(type) { case data.GitHubTransformer: handle = func(entry unmarshal.GitHubAdvisory) ([]data.Entry, error) { return t(entry) } case data.GitHubTransformerV2: handle = func(entry unmarshal.GitHubAdvisory) ([]data.Entry, error) { return t(entry, state) } } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty GHSA entry") continue } transformedEntries, err := handle(entry) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p githubProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "github-security-advisory") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse GHSA schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/github_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockGithubProcessorTransform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestGitHubProcessor_Process(t *testing.T) { f, err := os.Open("testdata/github.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewGitHubProcessor(mockGithubProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "github", }) assert.NoError(t, err) assert.Len(t, entries, 3) } func TestGithubProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/vulnerability/github-security-advisory/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.2.3", schemaURL: "https://example.com/vunnel/path/vulnerability/github-security-advisory/schema-1.2.3.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/vulnerability/github-security-advisory/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/vulnerability/github-security-advisory/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, } p := githubProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/kev_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type kevProcessor struct { transformer data.KnownExploitedVulnerabilityTransformerV2 } func NewV2KEVProcessor(transformer data.KnownExploitedVulnerabilityTransformerV2) data.Processor { return &kevProcessor{ transformer: transformer, } } func (p kevProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.KnownExploitedVulnerabilityEntries(reader) if err != nil { return nil, err } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty KEV entry") continue } transformedEntries, err := p.transformer(entry, state) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p kevProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "known-exploited") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse KEV schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/kev_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" ) func mockKEVProcessorTransform(vulnerability unmarshal.KnownExploitedVulnerability, state provider.State) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestKEVProcessor_Process(t *testing.T) { f, err := os.Open("testdata/kev.json") require.NoError(t, err) defer f.Close() processor := NewV2KEVProcessor(mockKEVProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "kev", }) assert.NoError(t, err) assert.Len(t, entries, 4) } func TestKEVProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/vulnerability/known-exploited/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.2.3", schemaURL: "https://example.com/vunnel/path/vulnerability/known-exploited/schema-1.2.3.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/vulnerability/known-exploited/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/vulnerability/known-exploited/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, { name: "invalid schema segment", schemaURL: "https://example.com/vunnel/path/vulnerability/not-kev/schema-1.0.0.json", expected: false, }, } p := kevProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/match_exclusion_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type matchExclusionProcessor struct { transformer data.MatchExclusionTransformer } func NewMatchExclusionProcessor(transformer data.MatchExclusionTransformer) data.Processor { return &matchExclusionProcessor{ transformer: transformer, } } func (p matchExclusionProcessor) Process(reader io.Reader, _ provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.MatchExclusions(reader) if err != nil { return nil, err } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty match-exclusion entry") continue } transformedEntries, err := p.transformer(entry) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p matchExclusionProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "match-exclusion") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse match-exclusion schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/match_exclusion_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockMatchExclusionProcessorTransform(vulnerability unmarshal.MatchExclusion) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestMatchExclusionProcessor_Process(t *testing.T) { f, err := os.Open("testdata/exclusions.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewMatchExclusionProcessor(mockMatchExclusionProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "match-exclusions", }) require.NoError(t, err) assert.Len(t, entries, 3) } func TestMatchExclusionProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/match-exclusion/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.3.4", schemaURL: "https://example.com/vunnel/path/match-exclusion/schema-1.3.4.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/match-exclusion/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/match-exclusion/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, } p := matchExclusionProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/msrc_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) // msrcProcessor defines the regular expression needed to signal what is supported type msrcProcessor struct { transformer any } // NewMSRCProcessor creates a new instance of msrcProcessor particular to MSRC func NewMSRCProcessor(transformer data.MSRCTransformer) data.Processor { return &msrcProcessor{ transformer: transformer, } } func NewV2MSRCProcessor(transformer data.MSRCTransformerV2) data.Processor { return &msrcProcessor{ transformer: transformer, } } func (p msrcProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.MSRCVulnerabilityEntries(reader) if err != nil { return nil, err } var handle func(entry unmarshal.MSRCVulnerability) ([]data.Entry, error) switch t := p.transformer.(type) { case data.MSRCTransformer: handle = func(entry unmarshal.MSRCVulnerability) ([]data.Entry, error) { return t(entry) } case data.MSRCTransformerV2: handle = func(entry unmarshal.MSRCVulnerability) ([]data.Entry, error) { return t(entry, state) } } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty MSRC entry") continue } transformedEntries, err := handle(entry) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p msrcProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "msrc") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse MSRC schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/msrc_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockMSRCProcessorTransform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestMSRCProcessor_Process(t *testing.T) { f, err := os.Open("testdata/msrc.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewMSRCProcessor(mockMSRCProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "msrc", }) require.NoError(t, err) assert.Len(t, entries, 2) } func TestMsrcProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/vulnerability/msrc/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.2.3", schemaURL: "https://example.com/vunnel/path/vulnerability/msrc/schema-1.2.3.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/vulnerability/msrc/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/vulnerability/msrc/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, } p := msrcProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/nvd_processor.go ================================================ package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type nvdProcessor struct { transformer any } func NewNVDProcessor(transformer data.NVDTransformer) data.Processor { return &nvdProcessor{ transformer: transformer, } } func NewV2NVDProcessor(transformer data.NVDTransformerV2) data.Processor { return &nvdProcessor{ transformer: transformer, } } func (p nvdProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.NvdVulnerabilityEntries(reader) if err != nil { return nil, err } var handle func(entry unmarshal.NVDVulnerability) ([]data.Entry, error) switch t := p.transformer.(type) { case data.NVDTransformer: handle = func(entry unmarshal.NVDVulnerability) ([]data.Entry, error) { return t(entry) } case data.NVDTransformerV2: handle = func(entry unmarshal.NVDVulnerability) ([]data.Entry, error) { return t(entry, state) } } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty NVD entry") continue } transformedEntries, err := handle(entry.Cve) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p nvdProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "nvd") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse NVD schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/nvd_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockNVDProcessorTransform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestNVDProcessor_Process(t *testing.T) { f, err := os.Open("testdata/nvd.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewNVDProcessor(mockNVDProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "nvd", }) require.NoError(t, err) assert.Len(t, entries, 3) } func TestNvdProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/vulnerability/nvd/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.4.7", schemaURL: "https://example.com/vunnel/path/vulnerability/nvd/schema-1.4.7.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/vulnerability/nvd/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/vulnerability/nvd/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, } p := nvdProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/openvex_processor.go ================================================ package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type openVEXProcessor struct { transformer data.OpenVEXTransformerV2 } func NewV2OpenVEXProcessor(transformer data.OpenVEXTransformerV2) data.Processor { return &openVEXProcessor{ transformer: transformer, } } func (p openVEXProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.OpenVEXVulnerabilityEntries(reader) if err != nil { return nil, err } for _, entry := range entries { transformedEntries, err := p.transformer(entry, state) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p openVEXProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "openvex") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse OpenVEX schema version") return false } // OpenVEX at 0.2.X (https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md) return parsedVersion.Major == 0 && parsedVersion.Minor >= 2 } ================================================ FILE: grype/db/processors/openvex_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockOpenVEXProcessorTransform(vulnerability unmarshal.OpenVEXVulnerability, _ provider.State) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestV2OpenVEXProcessor_Process(t *testing.T) { f, err := os.Open("testdata/openvex.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewV2OpenVEXProcessor(mockOpenVEXProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "openvex", }) require.NoError(t, err) assert.Len(t, entries, 1) } func TestOpenVEXProcessor_IsSupported(t *testing.T) { tests := []struct { name string schemaURL string want bool }{ { name: "one actually used by vunnel is supported", schemaURL: "https://github.com/openvex/spec/openvex_json_schema_0.2.0.json", want: true, }, { name: "openvex schema 0.2.1 is supported", schemaURL: "https://github.com/openvex/spec/openvex_json_schema_0.2.1.json", want: true, }, { name: "openvex schema 0.3.1 is supported", schemaURL: "https://github.com/openvex/spec/openvex_json_schema_0.3.1.json", want: true, }, { name: "openvex schema 0.1.1 is not supported", schemaURL: "https://github.com/openvex/spec/openvex_json_schema_0.1.1.json", want: false, }, { name: "higher schema is not supported", schemaURL: "https://github.com/openvex/spec/openvex_json_schema_1.2.0.json", want: false, }, { name: "non-openvex schema is not supported", schemaURL: "https://example.com/nvd/schema-1.4.0.json", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewV2OpenVEXProcessor(mockOpenVEXProcessorTransform) assert.Equal(t, tt.want, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/os_processor.go ================================================ //nolint:dupl package processors import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type osProcessor struct { transformer any } func NewOSProcessor(transformer data.OSTransformer) data.Processor { return &osProcessor{ transformer: transformer, } } func NewV2OSProcessor(transformer data.OSTransformerV2) data.Processor { return &osProcessor{ transformer: transformer, } } func (p osProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.OSVulnerabilityEntries(reader) if err != nil { return nil, err } var handle func(entry unmarshal.OSVulnerability) ([]data.Entry, error) switch t := p.transformer.(type) { case data.OSTransformer: handle = func(entry unmarshal.OSVulnerability) ([]data.Entry, error) { return t(entry) } case data.OSTransformerV2: handle = func(entry unmarshal.OSVulnerability) ([]data.Entry, error) { return t(entry, state) } } for _, entry := range entries { if entry.IsEmpty() { log.Warn("dropping empty OS entry") continue } transformedEntries, err := handle(entry) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p osProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "os") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse OS schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/os_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockOSProcessorTransform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestOSProcessor_Process(t *testing.T) { f, err := os.Open("testdata/os.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewOSProcessor(mockOSProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "rhel", }) require.NoError(t, err) assert.Len(t, entries, 4) } func TestOsProcessor_IsSupported(t *testing.T) { tc := []struct { name string schemaURL string expected bool }{ { name: "valid schema URL with version 1.0.0", schemaURL: "https://example.com/vunnel/path/vulnerability/os/schema-1.0.0.json", expected: true, }, { name: "valid schema URL with version 1.5.2", schemaURL: "https://example.com/vunnel/path/vulnerability/os/schema-1.5.2.json", expected: true, }, { name: "invalid schema URL with unsupported version", schemaURL: "https://example.com/vunnel/path/vulnerability/os/schema-2.0.0.json", expected: false, }, { name: "invalid schema URL with missing version", schemaURL: "https://example.com/vunnel/path/vulnerability/os/schema.json", expected: false, }, { name: "completely invalid URL", schemaURL: "https://example.com/invalid/schema/url", expected: false, }, } p := osProcessor{} for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/osv_processor.go ================================================ package processors // nolint:dupl import ( "io" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" "github.com/anchore/grype/internal/log" ) type osvProcessor struct { transformer data.OSVTransformerV2 } func NewV2OSVProcessor(transformer data.OSVTransformerV2) data.Processor { return &osvProcessor{ transformer: transformer, } } func (p osvProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) { var results []data.Entry entries, err := unmarshal.OSVVulnerabilityEntries(reader) if err != nil { return nil, err } for _, entry := range entries { transformedEntries, err := p.transformer(entry, state) if err != nil { return nil, err } results = append(results, transformedEntries...) } return results, nil } func (p osvProcessor) IsSupported(schemaURL string) bool { if !hasSchemaSegment(schemaURL, "osv") { return false } parsedVersion, err := parseVersion(schemaURL) if err != nil { log.WithFields("schema", schemaURL, "error", err).Error("failed to parse NVD schema version") return false } return parsedVersion.Major == 1 } ================================================ FILE: grype/db/processors/osv_processor_test.go ================================================ package processors import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" ) func mockOSVProcessorTransform(vulnerability unmarshal.OSVVulnerability, state provider.State) ([]data.Entry, error) { return []data.Entry{ { DBSchemaVersion: 0, Data: vulnerability, }, }, nil } func TestV2OSVProcessor_Process(t *testing.T) { f, err := os.Open("testdata/osv.json") require.NoError(t, err) defer testutil.CloseFile(f) processor := NewV2OSVProcessor(mockOSVProcessorTransform) entries, err := processor.Process(f, provider.State{ Provider: "osv", }) require.NoError(t, err) assert.Len(t, entries, 2) } func TestOSVProcessor_IsSupported(t *testing.T) { tests := []struct { name string schemaURL string want bool }{ { name: "one actually used by vunnel is supported", schemaURL: "https://raw.githubusercontent.com/anchore/vunnel/main/schema/vulnerability/osv/schema-1.5.0.json", want: true, }, { name: "osv schema 1.6.1 is supported", schemaURL: "https://example.com/osv/schema-1.6.1.json", want: true, }, { name: "osv schema 1.5.0 is supported", schemaURL: "https://example.com/osv/schema-1.5.0.json", want: true, }, { name: "lower major version is not supported", schemaURL: "https://example.com/osv/schema-0.4.0.json", want: false, }, { name: "higher schema is not supported", schemaURL: "https://example.com/osv/schema-2.4.0.json", want: false, }, { name: "non-osv schema is not supported", schemaURL: "https://example.com/nvd/schema-1.4.0.json", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewV2OSVProcessor(mockOSVProcessorTransform) assert.Equal(t, tt.want, p.IsSupported(tt.schemaURL)) }) } } ================================================ FILE: grype/db/processors/testdata/eol-with-empty.json ================================================ [ { "product": "", "identifiers": [], "name": "", "isEol": false, "isMaintained": false }, { "product": "alpine", "identifiers": [], "name": "3.18", "codename": null, "label": "3.18", "releaseDate": "2023-05-09", "isLts": false, "ltsFrom": null, "isEoas": false, "eoasFrom": null, "isEol": false, "eolFrom": "2025-05-09", "isMaintained": true } ] ================================================ FILE: grype/db/processors/testdata/eol.json ================================================ [ { "product": "ubuntu", "identifiers": [], "name": "22.04", "codename": "Jammy Jellyfish", "label": "22.04 LTS (Jammy Jellyfish)", "releaseDate": "2022-04-21", "isLts": true, "ltsFrom": "2022-04-21", "isEoas": false, "eoasFrom": null, "isEol": false, "eolFrom": "2027-04-01", "isMaintained": true }, { "product": "debian", "identifiers": [], "name": "11", "codename": "Bullseye", "label": "11 (Bullseye)", "releaseDate": "2021-08-14", "isLts": false, "ltsFrom": null, "isEoas": false, "eoasFrom": null, "isEol": false, "eolFrom": "2026-08-14", "isMaintained": true } ] ================================================ FILE: grype/db/processors/testdata/epss.json ================================================ [ { "cve": "CVE-2025-0108", "epss": 0.328, "percentile": 0.9929, "date": "2025-02-18" }, { "cve": "CVE-2025-0109", "epss": 0.283, "percentile": 0.9297, "date": "2025-02-18" } ] ================================================ FILE: grype/db/processors/testdata/exclusions.json ================================================ [ { }, { "id": "CVE-1234-5678", "justification": "CVE-1234-5678 is imaginary" }, { "id": "CVE-2012-abcxyz", "constraints": [ { "namespaces": [ "nvd:cpe", "abc.xyz:python" ] } ], "justification": "some reason" }, { "id": "CVE-2015-abc123", "constraints": [ { "ecosystem_constraints": [ { "language": "python", "package_constraints": [ { "package_name": "clock" } ] } ] } ], "justification": "" } ] ================================================ FILE: grype/db/processors/testdata/github.json ================================================ [ { "Advisory": { "Classification": "GENERAL", "Severity": "Critical", "CVSS": { "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "base_metrics": { "base_score": 9.8, "exploitability_score": 3.9, "impact_score": 5.9, "base_severity": "Critical" }, "status": "N/A" }, "FixedIn": [ { "name": "scratch-vm", "identifier": "0.2.0-prerelease.20200714185213", "ecosystem": "npm", "namespace": "github:npm", "range": "<= 0.2.0-prerelease.20200709173451" } ], "Summary": "Remote Code Execution in scratch-vm", "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", "CVE": [ "CVE-2020-14000" ], "Metadata": { "CVE": [ "CVE-2020-14000" ] }, "ghsaId": "GHSA-vc9j-fhvv-8vrf", "published": "2020-07-27T19:55:52Z", "updated": "2023-01-09T05:03:39Z", "withdrawn": null, "namespace": "github:npm" }, "Vulnerability": {} }, { "Advisory": { "CVE": [ "CVE-2018-8768" ], "FixedIn": [ { "ecosystem": "python", "identifier": "5.4.1", "name": "notebook", "namespace": "github:python", "range": "< 5.4.1" } ], "Metadata": { "CVE": [ "CVE-2018-8768" ] }, "Severity": "Low", "Summary": "Low severity vulnerability that affects notebook", "ghsaId": "GHSA-6cwv-x26c-w2q4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", "withdrawn": "2022-01-31T14:32:09Z" }, "Vulnerability": {} }, { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" }, { "ecosystem": "python", "identifier": "5.1b1", "name": "Plone", "namespace": "github:python", "range": ">= 5.1a1 < 5.1b1" }, { "ecosystem": "python", "identifier": "5.0.7", "name": "Plone-debug", "namespace": "github:python", "range": ">= 5.0rc1 < 5.0.7" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "GHSA-p5wr-vp8g-q5p4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} }, { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" }, { "ecosystem": "python", "identifier": "5.1b1", "name": "Plone", "namespace": "github:python", "range": ">= 5.1a1 < 5.1b1" }, { "ecosystem": "python", "identifier": "5.0.7", "name": "Plone-debug", "namespace": "github:python", "range": ">= 5.0rc1 < 5.0.7" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} }, { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" }, { "ecosystem": "python", "identifier": "5.1b1", "name": "Plone", "namespace": "github:python", "range": ">= 5.1a1 < 5.1b1" }, { "ecosystem": "python", "identifier": "5.0.7", "name": "Plone-debug", "namespace": "github:python", "range": ">= 5.0rc1 < 5.0.7" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} } ] ================================================ FILE: grype/db/processors/testdata/kev.json ================================================ [ { "cveID": "CVE-2025-24989", "vendorProject": "Microsoft", "product": "Power Pages", "vulnerabilityName": "Microsoft Power Pages Improper Access Control Vulnerability", "dateAdded": "2025-02-21", "shortDescription": "Microsoft Power Pages contains an improper access control vulnerability that allows an unauthorized attacker to elevate privileges over a network potentially bypassing the user registration control.", "requiredAction": "Apply mitigations per vendor instructions, follow BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.", "dueDate": "2025-03-14", "knownRansomwareCampaignUse": "Unknown", "notes": "https:\/\/msrc.microsoft.com\/update-guide\/en-US\/advisory\/CVE-2025-24989 ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-24989", "cwes": [ "CWE-284" ] }, { "cveID": "CVE-2025-0111", "vendorProject": "Palo Alto Networks", "product": "PAN-OS", "vulnerabilityName": "Palo Alto Networks PAN-OS File Read Vulnerability", "dateAdded": "2025-02-20", "shortDescription": "Palo Alto Networks PAN-OS contains an external control of file name or path vulnerability. Successful exploitation enables an authenticated attacker with network access to the management web interface to read files on the PAN-OS filesystem that are readable by the \u201cnobody\u201d user.", "requiredAction": "Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.", "dueDate": "2025-03-13", "knownRansomwareCampaignUse": "Unknown", "notes": "https:\/\/security.paloaltonetworks.com\/CVE-2025-0111 ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-0111", "cwes": [ "CWE-73" ] }, { "cveID": "CVE-2025-23209", "vendorProject": "Craft CMS", "product": "Craft CMS", "vulnerabilityName": "Craft CMS Code Injection Vulnerability", "dateAdded": "2025-02-20", "shortDescription": "Craft CMS contains a code injection vulnerability caused by improper validation of the database backup path, ultimately enabling remote code execution.", "requiredAction": "Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.", "dueDate": "2025-03-13", "knownRansomwareCampaignUse": "Unknown", "notes": "https:\/\/github.com\/craftcms\/cms\/security\/advisories\/GHSA-x684-96hh-833x ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-23209", "cwes": [ "CWE-94" ] }, { "cveID": "CVE-2025-0108", "vendorProject": "Palo Alto Networks", "product": "PAN-OS", "vulnerabilityName": "Palo Alto Networks PAN-OS Authentication Bypass Vulnerability", "dateAdded": "2025-02-18", "shortDescription": "Palo Alto Networks PAN-OS contains an authentication bypass vulnerability in its management web interface. This vulnerability allows an unauthenticated attacker with network access to the management web interface to bypass the authentication normally required and invoke certain PHP scripts.", "requiredAction": "Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.", "dueDate": "2025-03-11", "knownRansomwareCampaignUse": "Unknown", "notes": "https:\/\/security.paloaltonetworks.com\/CVE-2025-0108 ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-0108", "cwes": [ "CWE-306" ] } ] ================================================ FILE: grype/db/processors/testdata/msrc.json ================================================ [ { "cvss": { "base_score": 7.8, "temporal_score": 7, "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4493470", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", "https://support.microsoft.com/help/4493470" ] }, { "id": "4494440", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", "https://support.microsoft.com/help/4494440" ] }, { "id": "4503267", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", "https://support.microsoft.com/en-us/help/4503267" ] }, { "id": "4507460", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", "https://support.microsoft.com/help/4507460" ] }, { "id": "4512517", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", "https://support.microsoft.com/help/4512517" ] }, { "id": "4516044", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", "https://support.microsoft.com/help/4516044" ] } ], "id": "CVE-2019-0671", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "High", "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", "vulnerable": [ "4480961", "4483229", "4487026", "4489882" ] }, { "cvss": { "base_score": 4.4, "temporal_score": 4, "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4093119", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" ] }, { "id": "4103723", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" ] }, { "id": "4284880", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" ] }, { "id": "4338814", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" ] }, { "id": "4343887", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" ] }, { "id": "4345418", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" ] }, { "id": "4457131", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" ] }, { "id": "4462917", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" ] }, { "id": "4467691", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" ] }, { "id": "4471321", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" ] } ], "id": "CVE-2018-8116", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "Medium", "summary": "Microsoft Graphics Component Denial of Service Vulnerability", "vulnerable": [ "3213986", "4013429", "4015217", "4019472", "4022715", "4025339", "4034658", "4038782", "4041691", "4048953", "4053579", "4056890", "4074590", "4088787" ] }, { "cvss": { "base_score": 4.4, "temporal_score": 4, "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4093119", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" ] }, { "id": "4103723", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" ] }, { "id": "4284880", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" ] }, { "id": "4338814", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" ] }, { "id": "4343887", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" ] }, { "id": "4345418", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" ] }, { "id": "4457131", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" ] }, { "id": "4462917", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" ] }, { "id": "4467691", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" ] }, { "id": "4471321", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" ] } ], "id": "", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "Medium", "summary": "Microsoft Graphics Component Denial of Service Vulnerability", "vulnerable": [ "3213986", "4013429", "4015217", "4019472", "4022715", "4025339", "4034658", "4038782", "4041691", "4048953", "4053579", "4056890", "4074590", "4088787" ] }, { "cvss": { "base_score": 4.4, "temporal_score": 4, "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4093119", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" ] }, { "id": "4103723", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" ] }, { "id": "4284880", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" ] }, { "id": "4338814", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" ] }, { "id": "4343887", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" ] }, { "id": "4345418", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" ] }, { "id": "4457131", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" ] }, { "id": "4462917", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" ] }, { "id": "4467691", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" ] }, { "id": "4471321", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" ] } ], "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "Medium", "summary": "Microsoft Graphics Component Denial of Service Vulnerability", "vulnerable": [ "3213986", "4013429", "4015217", "4019472", "4022715", "4025339", "4034658", "4038782", "4041691", "4048953", "4053579", "4056890", "4074590", "4088787" ] } ] ================================================ FILE: grype/db/processors/testdata/nvd.json ================================================ [ { "cve": { "id": "CVE-1987-1111", "sourceIdentifier": "cve@mitre.org", "published": "2016-11-22T17:59:00.180", "lastModified": "2016-11-28T19:50:59.600", "vulnStatus": "Modified", "descriptions": [], "metrics": { "cvssMetricV30": [], "cvssMetricV2": [] }, "weaknesses": [], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", "versionEndIncluding": "1.14", "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" } ] } ] } ], "references": [] } }, { "cve": { "id": "CVE-1987-2222", "sourceIdentifier": "cve@mitre.org", "published": "2016-11-22T17:59:00.180", "lastModified": "2016-11-28T19:50:59.600", "vulnStatus": "Modified", "descriptions": [], "metrics": { "cvssMetricV30": [], "cvssMetricV2": [] }, "weaknesses": [], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", "versionEndIncluding": "1.14", "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" } ] } ] } ], "references": [] } }, { "cve": { "id": "CVE-1987-3333", "sourceIdentifier": "cve@mitre.org", "published": "2016-11-22T17:59:00.180", "lastModified": "2016-11-28T19:50:59.600", "vulnStatus": "Modified", "descriptions": [], "metrics": { "cvssMetricV30": [], "cvssMetricV2": [] }, "weaknesses": [], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", "versionEndIncluding": "1.14", "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" } ] } ] } ], "references": [] } }, { "cve": { "id": "", "sourceIdentifier": "^ note... there is no CVE ID in this test!!!", "published": "2016-11-22T17:59:00.180", "lastModified": "2016-11-28T19:50:59.600", "vulnStatus": "Modified", "descriptions": [], "metrics": { "cvssMetricV30": [], "cvssMetricV2": [] }, "weaknesses": [], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", "versionEndIncluding": "1.14", "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" } ] } ] } ], "references": [] } } ] ================================================ FILE: grype/db/processors/testdata/openvex.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-9f343308bfdc28ef92df6b952671127b25fbc1aa202e706467fee6ce7f9cb777", "author": "Unknown Author", "timestamp": "2025-08-01T15:13:37.914697-07:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2023-45803" }, "timestamp": "2025-08-01T15:13:37.914697-07:00", "products": [ { "identifiers": { "purl": "pkg:pypi/urllib3@1.26.16+cgr.1" } } ], "status": "fixed" } ] } ================================================ FILE: grype/db/processors/testdata/oracle.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "libexif", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-devel", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-dummy", "NamespaceName": "ol:8", "Version": "None", "VersionFormat": "rpm" } ], "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", "Metadata": { "CVE": [ { "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", "Name": "CVE-2020-13112" } ], "Issued": "2020-06-15", "RefId": "ELSA-2020-2550" }, "Name": "ELSA-2020-2550", "NamespaceName": "ol:8", "Severity": "Medium" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "libexif", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-devel", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-dummy", "NamespaceName": "ol:8", "Version": "None", "VersionFormat": "rpm" } ], "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", "Metadata": { "CVE": [ { "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", "Name": "CVE-2020-13112" } ], "Issued": "2020-06-15", "RefId": "ELSA-2020-2550" }, "Name": "", "NamespaceName": "ol:8", "Severity": "Medium" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "libexif", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-devel", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-dummy", "NamespaceName": "ol:8", "Version": "None", "VersionFormat": "rpm" } ], "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", "Metadata": { "CVE": [ { "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", "Name": "CVE-2020-13112" } ], "Issued": "2020-06-15", "RefId": "ELSA-2020-2550" }, "NamespaceName": "ol:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/processors/testdata/os.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "asterisk", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "1:1.6.2.0~rc3-1", "VersionFormat": "dpkg" }, { "Name": "auth2db", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "0.2.5-2+dfsg-1", "VersionFormat": "dpkg" }, { "Name": "exaile", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "0.2.14+debian-2.2", "VersionFormat": "dpkg" }, { "Name": "wordpress", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", "Metadata": { "NVD": { "CVSSv2": { "Score": 7.5, "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" } } }, "Name": "CVE-2008-7220", "NamespaceName": "debian:8", "Severity": "High" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "rsyslog", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "5.7.4-1", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", "Metadata": { "NVD": { "CVSSv2": { "Score": 2.1, "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" } } }, "Name": "CVE-2011-4623", "NamespaceName": "debian:8", "Severity": "Low" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "rsyslog", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "3.18.6-1", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", "Metadata": { "NVD": { "CVSSv2": { "Score": 5, "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" } } }, "Name": "CVE-2008-5618", "NamespaceName": "debian:8", "Severity": "Low" } }, { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "389-ds-base", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-debuginfo", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-devel", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-libs", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-snmp", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" } ], "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", "Metadata": { "CVE": [ {"Name": "CVE-2018-14648"} ] }, "Name": "ALAS-2018-1106", "NamespaceName": "amzn:2", "Severity": "Medium" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [], "Link": null, "Metadata": {}, "Name": null, "NamespaceName": null, "Severity": null } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [], "Link": null, "Metadata": {}, "Name": "", "NamespaceName": null, "Severity": null } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [], "Link": null, "Metadata": {}, "NamespaceName": null, "Severity": null } } ] ================================================ FILE: grype/db/processors/testdata/osv.json ================================================ [ { "schema_version": "1.3.1", "id": "GO-2023-2412", "modified": "0001-01-01T00:00:00Z", "published": "0001-01-01T00:00:00Z", "aliases": [ "GHSA-7ww5-4wqc-m92c" ], "summary": "RAPL accessibility in github.com/containerd/containerd", "details": "RAPL accessibility in github.com/containerd/containerd", "affected": [ { "package": { "name": "github.com/containerd/containerd", "ecosystem": "Go" }, "ranges": [ { "type": "SEMVER", "events": [ { "introduced": "0" }, { "fixed": "1.6.26" }, { "introduced": "1.7.0" }, { "fixed": "1.7.11" } ] } ], "ecosystem_specific": { "imports": [ { "path": "github.com/containerd/containerd/contrib/apparmor", "symbols": [ "DumpDefaultProfile", "LoadDefaultProfile", "generate" ] } ] } } ], "references": [ { "type": "ADVISORY", "url": "https://github.com/containerd/containerd/security/advisories/GHSA-7ww5-4wqc-m92c" }, { "type": "FIX", "url": "https://github.com/containerd/containerd/commit/67d356cb3095f3e8f8ad7d36f9a733fea1e7e28c" }, { "type": "FIX", "url": "https://github.com/containerd/containerd/commit/746b910f05855c8bfdb4415a1c0f958b234910e5" } ], "database_specific": { "url": "https://pkg.go.dev/vuln/GO-2023-2412" } }, { "schema_version": "1.3.1", "id": "GO-2023-2413", "modified": "0001-01-01T00:00:00Z", "published": "0001-01-01T00:00:00Z", "aliases": [ "CVE-2023-49922", "GHSA-hj4r-2c9c-29h3" ], "summary": "Sensitive information logged in github.com/elastic/beats/v7", "details": "Sensitive information logged in github.com/elastic/beats/v7", "affected": [ { "package": { "name": "github.com/elastic/beats/v7", "ecosystem": "Go" }, "ranges": [ { "type": "SEMVER", "events": [ { "introduced": "0" }, { "fixed": "7.17.16" } ] } ], "ecosystem_specific": { "imports": [ { "path": "github.com/elastic/beats/v7/libbeat/processors/script/javascript", "symbols": [ "jsProcessor.Run", "session.runProcessFunc" ] } ] } } ], "references": [ { "type": "ADVISORY", "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-49922" }, { "type": "FIX", "url": "https://github.com/elastic/beats/commit/9bd7de84ab9c31bb4e1c0a348a7b7c26817a0996" }, { "type": "WEB", "url": "https://discuss.elastic.co/t/beats-and-elastic-agent-8-11-3-7-17-16-security-update-esa-2023-30/349180" } ], "database_specific": { "url": "https://pkg.go.dev/vuln/GO-2023-2413" } } ] ================================================ FILE: grype/db/processors/version.go ================================================ package processors import ( "fmt" "regexp" "strconv" "strings" ) var schemaFilePattern = regexp.MustCompile(`schema([-_])(?P\d+)\.(?P\d+)\.(?P\d+)\.json`) type version struct { Major int Minor int Patch int } func parseVersion(schemaURL string) (*version, error) { matches := schemaFilePattern.FindStringSubmatch(schemaURL) if matches == nil { return nil, fmt.Errorf("invalid version format in URL: %s", schemaURL) } v := &version{} for i, name := range schemaFilePattern.SubexpNames() { if name == "" { continue } value, err := strconv.Atoi(matches[i]) if err != nil { return nil, fmt.Errorf("failed to parse %s: %v", name, err) } switch name { case "major": v.Major = value case "minor": v.Minor = value case "patch": v.Patch = value } } return v, nil } func hasSchemaSegment(schemaURL string, segment string) bool { return strings.Contains(schemaURL, "/"+segment+"/") } ================================================ FILE: grype/db/processors/version_test.go ================================================ package processors import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseVersion(t *testing.T) { tests := []struct { name string schemaURL string expected *version wantErr bool }{ { name: "valid version 1.0.0", schemaURL: "https://example.com/vunnel/path/schema-1.0.0.json", expected: &version{Major: 1, Minor: 0, Patch: 0}, wantErr: false, }, { name: "valid version 2.3.4", schemaURL: "https://example.com/vunnel/path/schema-2.3.4.json", expected: &version{Major: 2, Minor: 3, Patch: 4}, wantErr: false, }, { name: "missing patch version", schemaURL: "https://example.com/vunnel/path/schema-1.0.json", expected: nil, wantErr: true, }, { name: "invalid format", schemaURL: "https://example.com/vunnel/path/schema.json", expected: nil, wantErr: true, }, { name: "non-numeric version", schemaURL: "https://example.com/vunnel/path/schema-1.a.0.json", expected: nil, wantErr: true, }, { name: "valid version with extra path", schemaURL: "https://example.com/vunnel/path/vulnerability/schema_1.2.3.json", expected: &version{Major: 1, Minor: 2, Patch: 3}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parseVersion(tt.schemaURL) if tt.wantErr { assert.Error(t, err) assert.Nil(t, result) } else { assert.NoError(t, err) assert.Equal(t, tt.expected, result) } }) } } ================================================ FILE: grype/db/provider/entry/file.go ================================================ package entry import ( "io" "os" ) type fileOpener struct { path string } func fileOpeners(resultPaths []string) <-chan Opener { openers := make(chan Opener) go func() { defer close(openers) for _, p := range resultPaths { openers <- fileOpener{path: p} } }() return openers } func (e fileOpener) Open() (io.ReadCloser, error) { return os.Open(e.path) } func (e fileOpener) String() string { return e.path } ================================================ FILE: grype/db/provider/entry/opener.go ================================================ package entry import ( "fmt" "io" ) type Opener interface { Open() (io.ReadCloser, error) fmt.Stringer } func Openers(store string, resultPaths []string) (<-chan Opener, int64, error) { switch store { case "flat-file": return fileOpeners(resultPaths), int64(len(resultPaths)), nil case "sqlite": return sqliteOpeners(resultPaths) } return nil, 0, fmt.Errorf("unknown store: %q", store) } func Count(store string, resultPaths []string) (int64, error) { switch store { case "flat-file": return int64(len(resultPaths)), nil case "sqlite": return sqliteEntryCount(resultPaths) } return 0, fmt.Errorf("unknown store: %q", store) } ================================================ FILE: grype/db/provider/entry/sqlite.go ================================================ package entry import ( "bytes" "context" "fmt" "io" "strings" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/anchore/grype/internal/log" ) var readOptions = []string{ "immutable=1", "cache=shared", "mode=ro", } // note: the name of the struct is tied to the table name type results struct { ID string `gorm:"column:id"` Record []byte `gorm:"column:record"` } type bytesOpener struct { contents []byte name string } type errorOpener struct { err error } func sqliteEntryCount(resultPaths []string) (int64, error) { var dbPath string for _, p := range resultPaths { // we should only be validating against a single results DB, not any DB in the output if strings.HasSuffix(p, "results.db") { dbPath = p break } } if dbPath == "" { return 0, fmt.Errorf("unable to find DB result file") } db, err := openDB(dbPath) if err != nil { return 0, err } var count int64 db.Model(&results{}).Count(&count) return count, nil } func sqliteOpeners(resultPaths []string) (<-chan Opener, int64, error) { var dbPath string for _, p := range resultPaths { if strings.HasSuffix(p, "results.db") { dbPath = p break } } if dbPath == "" { return nil, 0, fmt.Errorf("unable to find DB result file") } db, err := openDB(dbPath) if err != nil { return nil, 0, err } var count int64 db.Model(&results{}).Count(&count) openers := make(chan Opener) go func() { defer close(openers) var models []results current := 0 check := db.FindInBatches(&models, 100, func(_ *gorm.DB, _ int) error { for _, result := range models { openers <- bytesOpener{ contents: result.Record, name: result.ID, } } current += len(models) // log.WithFields("count", current).Trace("records read from the provider cache DB") // note: returning an error will stop future batches return nil }) if check.Error != nil { openers <- errorOpener{err: check.Error} } }() return openers, count, nil } func (e bytesOpener) Open() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(e.contents)), nil } func (e bytesOpener) String() string { return e.name } func (e errorOpener) Open() (io.ReadCloser, error) { return nil, e.err } func (e errorOpener) String() string { return e.err.Error() } // Open a new connection to a sqlite3 database file func openDB(path string) (*gorm.DB, error) { connStr, err := connectionString(path) if err != nil { return nil, err } // &immutable=1&cache=shared&mode=ro for _, o := range readOptions { connStr += fmt.Sprintf("&%s", o) } dbObj, err := gorm.Open(sqlite.Open(connStr), &gorm.Config{Logger: newLogger()}) if err != nil { return nil, fmt.Errorf("unable to connect to DB: %w", err) } return dbObj, nil } // ConnectionString creates a connection string for sqlite3 func connectionString(path string) (string, error) { if path == "" { return "", fmt.Errorf("no db filepath given") } return fmt.Sprintf("file:%s?cache=shared", path), nil } type logAdapter struct{} func newLogger() logger.Interface { return logAdapter{} } func (l logAdapter) LogMode(logger.LogLevel) logger.Interface { return l } func (l logAdapter) Info(_ context.Context, _ string, _ ...interface{}) { // unimplemented } func (l logAdapter) Warn(_ context.Context, fmt string, v ...interface{}) { log.Warnf("gorm: "+fmt, v...) } func (l logAdapter) Error(_ context.Context, fmt string, v ...interface{}) { log.Errorf("gorm: "+fmt, v...) } func (l logAdapter) Trace(_ context.Context, _ time.Time, _ func() (_ string, _ int64), _ error) { // unimplemented } ================================================ FILE: grype/db/provider/file.go ================================================ package provider import ( "os" "path/filepath" "github.com/OneOfOne/xxhash" "github.com/spf13/afero" "github.com/anchore/grype/internal/file" ) type File struct { Path string `json:"path"` Digest string `json:"digest"` Algorithm string `json:"algorithm"` } type Files []File func NewFile(path string) (*File, error) { digest, err := file.HashFile(afero.NewOsFs(), path, xxhash.New64()) if err != nil { return nil, err } return &File{ Path: path, Digest: digest, Algorithm: "xxh64", }, nil } func NewFiles(paths ...string) (Files, error) { var files []File for _, path := range paths { input, err := NewFile(path) if err != nil { return nil, err } files = append(files, *input) } return files, nil } func (i Files) Paths() []string { var paths []string for _, input := range i { paths = append(paths, input.Path) } return paths } func NewFilesFromDir(dir string) (Files, error) { listing, err := os.ReadDir(dir) if err != nil { return nil, err } var paths []string for _, f := range listing { if f.IsDir() { continue } paths = append(paths, filepath.Join(dir, f.Name())) } return NewFiles(paths...) } ================================================ FILE: grype/db/provider/model.go ================================================ package provider import ( "fmt" db "github.com/anchore/grype/grype/db/v6" ) func Model(state State) *db.Provider { var digest string if state.Listing != nil { if state.Listing.Algorithm != "" && state.Listing.Digest != "" { digest = state.Listing.Algorithm + ":" + state.Listing.Digest } } return &db.Provider{ ID: state.Provider, Version: fmt.Sprintf("%d", state.Version), Processor: state.Processor, DateCaptured: &state.Timestamp, InputDigest: digest, } } ================================================ FILE: grype/db/provider/model_test.go ================================================ package provider import ( "testing" "time" "github.com/stretchr/testify/require" db "github.com/anchore/grype/grype/db/v6" ) func TestProviderModel(t *testing.T) { tests := []struct { name string state State expected *db.Provider }{ { name: "valid state with listing", state: State{ Provider: "test-provider", Version: 2, Processor: "test-processor", Timestamp: time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC), Listing: &File{ Algorithm: "sha256", Digest: "abc123", }, }, expected: &db.Provider{ ID: "test-provider", Version: "2", Processor: "test-processor", DateCaptured: func() *time.Time { t := time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC); return &t }(), InputDigest: "sha256:abc123", }, }, { name: "valid state without listing", state: State{ Provider: "test-provider", Version: 1, Processor: "test-processor", Timestamp: time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC), Listing: nil, }, expected: &db.Provider{ ID: "test-provider", Version: "1", Processor: "test-processor", DateCaptured: func() *time.Time { t := time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC); return &t }(), InputDigest: "", }, }, { name: "valid state with empty listing fields", state: State{ Provider: "test-provider", Version: 3, Processor: "test-processor", Timestamp: time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC), Listing: &File{ Algorithm: "", Digest: "", }, }, expected: &db.Provider{ ID: "test-provider", Version: "3", Processor: "test-processor", DateCaptured: func() *time.Time { t := time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC); return &t }(), InputDigest: "", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Model(tt.state) require.Equal(t, tt.expected, result) }) } } ================================================ FILE: grype/db/provider/provider.go ================================================ package provider import ( "context" ) type Kind string type Reader interface { ID() Identifier State() (*State, error) } type Writer interface { Update(context.Context) error } type Identifier struct { Name string `yaml:"name" json:"name" mapstructure:"name"` Kind Kind `yaml:"kind,omitempty" json:"kind" mapstructure:"kind"` } type Providers []Reader func (ps Providers) Filter(names ...string) Providers { var filtered Providers for _, p := range ps { for _, name := range names { if p.ID().Name == name { filtered = append(filtered, p) } } } return filtered } type Collection struct { Root string Providers Providers } ================================================ FILE: grype/db/provider/state.go ================================================ package provider import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/spf13/afero" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" ) // data shape dictated by vunnel "provider workspace state" schema definition type State struct { location string root string Provider string `json:"provider"` Version int `json:"version"` DistributionVersion int `json:"distribution_version"` Processor string `json:"processor"` Schema Schema `json:"schema"` URLs []string `json:"urls"` Timestamp time.Time `json:"timestamp"` Listing *File `json:"listing"` Store string `json:"store"` Stale bool `json:"stale"` resultFileStates []File } type Schema struct { Version string `json:"version"` URL string `json:"url"` } type States []State func ReadState(location string) (*State, error) { by, err := os.ReadFile(location) if err != nil { return nil, err } var sd State if err := json.Unmarshal(by, &sd); err != nil { return nil, err } root := filepath.Dir(location) sd.root = root sd.location = location // we usually have a lot of records (depending on the source) sd.resultFileStates = make([]File, 0, 300000) start := time.Now() if sd.Listing != nil { algorithm := "xxh64" // sane default for performance // get extension from listing file extension := filepath.Ext(sd.Listing.Path) if extension != "" { algorithm = strings.TrimPrefix(extension, ".") } listingPath := filepath.Join(root, sd.Listing.Path) f, err := os.Open(listingPath) if err != nil { return nil, fmt.Errorf("unable to open listing file %q: %w", listingPath, err) } // note: bufio scanner is **much** faster than Fscan scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() index := strings.Index(line, " ") // faster than strings.Split if index != -1 { sd.resultFileStates = append(sd.resultFileStates, File{ Path: line[index+2:], Digest: line[:index], Algorithm: algorithm, }, ) } } } log.WithFields("duration", time.Since(start), "entries", len(sd.resultFileStates)).Trace("loaded result listing file") return &sd, nil } func (sd State) ResultPath(filename string) string { return filepath.Join(sd.root, filename) } func (sd State) ResultPaths() []string { var paths []string for _, p := range sd.resultFileStates { paths = append(paths, sd.ResultPath(p.Path)) } return paths } func (sd State) Verify(workspaceRoots ...string) error { if sd.root != "" { workspaceRoots = append(workspaceRoots, sd.root) } for _, workspaceRoot := range workspaceRoots { for _, resultConfig := range sd.resultFileStates { workspace := NewWorkspaceFromExisting(workspaceRoot) path := filepath.Join(workspace.Path(), resultConfig.Path) log.WithFields("path", resultConfig.Path, "provider", sd.Provider).Trace("validating result file") matches, _, err := file.ValidateByHash(afero.NewOsFs(), path, resultConfig.Algorithm+":"+resultConfig.Digest) if err != nil { return fmt.Errorf("unable to validate result file %q: %w", path, err) } if !matches { return fmt.Errorf("hash mismatch for result file %q", path) } } } return nil } func (s States) Names() []string { var names []string for _, state := range s { names = append(names, state.Provider) } return names } func (s States) EarliestTimestamp() (time.Time, error) { if len(s) == 0 { return time.Time{}, fmt.Errorf("cannot find earliest timestamp: no states provided") } // special case when there is exactly 1 state, return its timestamp even // if it is nvd, because otherwise quality gates that pull only nvd deterministically fail. if len(s) == 1 { return s[0].Timestamp, nil } var earliest time.Time for _, curState := range s { // the NVD api is constantly down, so we don't want to consider it for the earliest timestamp if curState.Provider == "nvd" { log.WithFields("provider", curState.Provider).Debug("not considering data age for provider") continue } if earliest.IsZero() { earliest = curState.Timestamp continue } if curState.Timestamp.Before(earliest) { earliest = curState.Timestamp } } if earliest.IsZero() { return time.Time{}, fmt.Errorf("unable to determine earliest timestamp") } log.WithFields("timestamp", earliest).Debug("earliest data timestamp") return earliest, nil } ================================================ FILE: grype/db/provider/state_test.go ================================================ package provider import ( "reflect" "testing" "time" "github.com/stretchr/testify/require" ) func Test_earliestTimestamp(t *testing.T) { tests := []struct { name string states []State want time.Time wantErr require.ErrorAssertionFunc }{ { name: "happy path", states: []State{ { Timestamp: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC), }, { Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { Timestamp: time.Date(2021, 1, 3, 0, 0, 0, 0, time.UTC), }, }, want: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { name: "empty states", states: []State{}, want: time.Time{}, wantErr: requireErrorContains("cannot find earliest timestamp: no states provided"), }, { name: "single state", states: []State{ { Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, }, want: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { name: "single state, but it's nvd", states: []State{ { Provider: "nvd", Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, }, want: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { name: "all states have provider nvd", states: []State{ { Provider: "nvd", Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { Provider: "nvd", Timestamp: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC), }, }, want: time.Time{}, wantErr: requireErrorContains("unable to determine earliest timestamp"), }, { name: "mix of nvd and non-nvd providers", states: []State{ { Provider: "nvd", Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { Provider: "other", Timestamp: time.Date(2021, 1, 3, 0, 0, 0, 0, time.UTC), }, { Provider: "other", Timestamp: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC), }, }, want: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC), }, { name: "timestamps are the same", states: []State{ { Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, { Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, }, want: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } got, err := States(tt.states).EarliestTimestamp() tt.wantErr(t, err) if err != nil { return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("earliestTimestamp() = %v, want %v", got, tt.want) } }) } } func requireErrorContains(text string) require.ErrorAssertionFunc { return func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.Error(t, err, msgAndArgs...) require.Contains(t, err.Error(), text, msgAndArgs...) } } ================================================ FILE: grype/db/provider/workspace.go ================================================ package provider import ( "path/filepath" ) type Workspace struct { Root string Name string } func NewWorkspace(root, name string) Workspace { return Workspace{ Root: root, Name: name, } } func NewWorkspaceFromExisting(workspacePath string) Workspace { return Workspace{ Root: filepath.Dir(workspacePath), Name: filepath.Base(workspacePath), } } func (w Workspace) Path() string { return filepath.Join(w.Root, w.Name) } func (w Workspace) StatePath() string { return filepath.Join(w.Path(), "metadata.json") } func (w Workspace) InputPath() string { return filepath.Join(w.Path(), "input") } func (w Workspace) ResultsPath() string { return filepath.Join(w.Path(), "results") } func (w Workspace) ReadState() (*State, error) { return ReadState(w.StatePath()) } ================================================ FILE: grype/db/v5/advisory.go ================================================ package v5 // Advisory represents published statements regarding a vulnerability (and potentially about its resolution). type Advisory struct { ID string `json:"id"` Link string `json:"link"` } ================================================ FILE: grype/db/v5/build/processors.go ================================================ package v5 import ( "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/processors" "github.com/anchore/grype/grype/db/v5/build/transformers/github" "github.com/anchore/grype/grype/db/v5/build/transformers/matchexclusions" "github.com/anchore/grype/grype/db/v5/build/transformers/msrc" "github.com/anchore/grype/grype/db/v5/build/transformers/nvd" "github.com/anchore/grype/grype/db/v5/build/transformers/os" ) type Config struct { NVD nvd.Config } type Option func(cfg *Config) func WithCPEParts(included []string) Option { return func(cfg *Config) { cfg.NVD.CPEParts = strset.New(included...) } } func WithInferNVDFixVersions(infer bool) Option { return func(cfg *Config) { cfg.NVD.InferNVDFixVersions = infer } } func NewConfig(options ...Option) Config { var cfg Config for _, option := range options { option(&cfg) } return cfg } func Processors(cfg Config) []data.Processor { return []data.Processor{ processors.NewGitHubProcessor(github.Transform), processors.NewMSRCProcessor(msrc.Transform), processors.NewNVDProcessor(nvd.Transformer(cfg.NVD)), processors.NewOSProcessor(os.Transform), processors.NewMatchExclusionProcessor(matchexclusions.Transform), } } ================================================ FILE: grype/db/v5/build/transformers/entry.go ================================================ package transformers import ( "github.com/anchore/grype/grype/db/data" db "github.com/anchore/grype/grype/db/v5" ) func NewEntries(vs []db.Vulnerability, metadata db.VulnerabilityMetadata) []data.Entry { entries := []data.Entry{ { DBSchemaVersion: db.SchemaVersion, Data: metadata, }, } for _, vuln := range vs { entries = append(entries, data.Entry{ DBSchemaVersion: db.SchemaVersion, Data: vuln, }) } return entries } ================================================ FILE: grype/db/v5/build/transformers/github/testdata/github-github-npm-0.json ================================================ { "Advisory": { "Classification": "GENERAL", "Severity": "Critical", "CVSS": { "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "base_metrics": { "base_score": 9.8, "exploitability_score": 3.9, "impact_score": 5.9, "base_severity": "Critical" }, "status": "N/A" }, "FixedIn": [ { "name": "scratch-vm", "identifier": "0.2.0-prerelease.20200714185213", "ecosystem": "npm", "namespace": "github:npm", "range": "<= 0.2.0-prerelease.20200709173451" } ], "Summary": "Remote Code Execution in scratch-vm", "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", "CVE": [ "CVE-2020-14000" ], "Metadata": { "CVE": [ "CVE-2020-14000" ] }, "ghsaId": "GHSA-vc9j-fhvv-8vrf", "published": "2020-07-27T19:55:52Z", "updated": "2023-01-09T05:03:39Z", "withdrawn": null, "namespace": "github:npm" } } ================================================ FILE: grype/db/v5/build/transformers/github/testdata/github-github-python-0.json ================================================ [ { "Advisory": { "CVE": [ "CVE-2018-8768" ], "FixedIn": [ { "ecosystem": "python", "identifier": "5.4.1", "name": "notebook", "namespace": "github:python", "range": "< 5.4.1" } ], "Metadata": { "CVE": [ "CVE-2018-8768" ] }, "Severity": "Low", "Summary": "Low severity vulnerability that affects notebook", "ghsaId": "GHSA-6cwv-x26c-w2q4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", "withdrawn": null }, "Vulnerability": {} }, { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "GHSA-p5wr-vp8g-q5p4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} } ] ================================================ FILE: grype/db/v5/build/transformers/github/testdata/github-github-python-1.json ================================================ { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" }, { "ecosystem": "python", "identifier": "5.1b1", "name": "Plone", "namespace": "github:python", "range": ">= 5.1a1 < 5.1b1" }, { "ecosystem": "python", "identifier": "5.0.7", "name": "Plone", "namespace": "github:python", "range": ">= 5.0rc1 < 5.0.7" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "GHSA-p5wr-vp8g-q5p4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} } ================================================ FILE: grype/db/v5/build/transformers/github/testdata/github-withdrawn.json ================================================ { "Advisory": { "CVE": [ "CVE-2018-8768" ], "FixedIn": [ { "ecosystem": "python", "identifier": "5.4.1", "name": "notebook", "namespace": "github:python", "range": "< 5.4.1" } ], "Metadata": { "CVE": [ "CVE-2018-8768" ] }, "Severity": "Low", "Summary": "Low severity vulnerability that affects notebook", "ghsaId": "GHSA-6cwv-x26c-w2q4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", "withdrawn": "2022-01-31T14:32:09Z" }, "Vulnerability": {} } ================================================ FILE: grype/db/v5/build/transformers/github/testdata/multiple-fixed-in-names.json ================================================ { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" }, { "ecosystem": "python", "identifier": "5.1b1", "name": "Plone", "namespace": "github:python", "range": ">= 5.1a1 < 5.1b1" }, { "ecosystem": "python", "identifier": "5.0.7", "name": "Plone-debug", "namespace": "github:python", "range": ">= 5.0rc1 < 5.0.7" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "GHSA-p5wr-vp8g-q5p4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} } ================================================ FILE: grype/db/v5/build/transformers/github/transform.go ================================================ package github import ( "errors" "fmt" "strings" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/build/transformers" "github.com/anchore/grype/grype/db/v5/namespace" syftPkg "github.com/anchore/syft/syft/pkg" ) var errSkip = fmt.Errorf("skipping advisory") func buildGrypeNamespace(group string) (namespace.Namespace, error) { feedGroupComponents := strings.Split(group, ":") if len(feedGroupComponents) < 2 { return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) } feedGroupLang := feedGroupComponents[1] syftLanguage := syftPkg.LanguageByName(feedGroupLang) if syftLanguage == syftPkg.UnknownLanguage { switch feedGroupLang { case "nuget": syftLanguage = syftPkg.Dotnet case "github-action", "erlang": // we don't want to error out on this, but grype at this version does not support these ecosystems return nil, errSkip default: return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) } } ns, err := namespace.FromString(fmt.Sprintf("github:language:%s", string(syftLanguage))) if err != nil { return nil, err } return ns, nil } func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { var allVulns []db.Vulnerability // Exclude entries marked as withdrawn if vulnerability.Advisory.Withdrawn != "" { return nil, nil } // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) recordSource := fmt.Sprintf("github:%s", vulnerability.Advisory.Namespace) grypeNamespace, err := buildGrypeNamespace(vulnerability.Advisory.Namespace) if err != nil { if errors.Is(err, errSkip) { return nil, nil } return nil, err } entryNamespace := grypeNamespace.String() // there may be multiple packages indicated within the FixedIn field, we should make // separate vulnerability entries (one for each name|namespaces combo) while merging // constraint ranges as they are found. for idx, fixedInEntry := range vulnerability.Advisory.FixedIn { constraint := versionutil.EnforceSemVerConstraint(fixedInEntry.Range) var versionFormat string switch entryNamespace { case "github:language:python": versionFormat = "python" default: versionFormat = "unknown" } // create vulnerability entry allVulns = append(allVulns, db.Vulnerability{ ID: vulnerability.Advisory.GhsaID, VersionConstraint: constraint, VersionFormat: versionFormat, RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), PackageName: grypeNamespace.Resolver().Normalize(fixedInEntry.Name), Namespace: entryNamespace, Fix: getFix(vulnerability, idx), }) } // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) metadata := db.VulnerabilityMetadata{ ID: vulnerability.Advisory.GhsaID, DataSource: vulnerability.Advisory.URL, Namespace: entryNamespace, RecordSource: recordSource, Severity: vulnerability.Advisory.Severity, URLs: []string{vulnerability.Advisory.URL}, Description: vulnerability.Advisory.Summary, Cvss: getCvss(vulnerability), } return transformers.NewEntries(allVulns, metadata), nil } func getFix(entry unmarshal.GitHubAdvisory, idx int) db.Fix { fixedInEntry := entry.Advisory.FixedIn[idx] var fixedInVersions []string fixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Identifier) if fixedInVersion != "" { fixedInVersions = append(fixedInVersions, fixedInVersion) } fixState := db.NotFixedState if len(fixedInVersions) > 0 { fixState = db.FixedState } return db.Fix{ Versions: fixedInVersions, State: fixState, } } func getRelatedVulnerabilities(entry unmarshal.GitHubAdvisory) []db.VulnerabilityReference { vulns := make([]db.VulnerabilityReference, len(entry.Advisory.CVE)) for idx, cve := range entry.Advisory.CVE { vulns[idx] = db.VulnerabilityReference{ ID: cve, Namespace: "nvd:cpe", } } return vulns } func getCvss(entry unmarshal.GitHubAdvisory) (cvss []db.Cvss) { if entry.Advisory.CVSS == nil { return cvss } cvss = append(cvss, db.Cvss{ Version: entry.Advisory.CVSS.Version, Vector: entry.Advisory.CVSS.VectorString, Metrics: db.NewCvssMetrics( entry.Advisory.CVSS.BaseMetrics.BaseScore, entry.Advisory.CVSS.BaseMetrics.ExploitabilityScore, entry.Advisory.CVSS.BaseMetrics.ImpactScore, ), VendorMetadata: transformers.VendorBaseMetrics{ BaseSeverity: entry.Advisory.CVSS.BaseMetrics.BaseSeverity, Status: entry.Advisory.CVSS.Status, }, }) return cvss } ================================================ FILE: grype/db/v5/build/transformers/github/transform_test.go ================================================ package github import ( "os" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" db "github.com/anchore/grype/grype/db/v5" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/build/transformers" "github.com/anchore/grype/grype/db/v5/namespace" "github.com/anchore/grype/grype/db/v5/namespace/language" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestBuildGrypeNamespace(t *testing.T) { tests := []struct { group string namespace namespace.Namespace wantErr require.ErrorAssertionFunc }{ { group: "github:python", namespace: language.NewNamespace("github", syftPkg.Python, ""), }, { group: "github:composer", namespace: language.NewNamespace("github", syftPkg.PHP, ""), }, { group: "github:gem", namespace: language.NewNamespace("github", syftPkg.Ruby, ""), }, { group: "github:npm", namespace: language.NewNamespace("github", syftPkg.JavaScript, ""), }, { group: "github:go", namespace: language.NewNamespace("github", syftPkg.Go, ""), }, { group: "github:nuget", namespace: language.NewNamespace("github", syftPkg.Dotnet, ""), }, { group: "github:rust", namespace: language.NewNamespace("github", syftPkg.Rust, ""), }, { group: "github:github-action", wantErr: func(t require.TestingT, err error, i ...interface{}) { assert.Error(t, err) assert.ErrorIs(t, errSkip, err) }, }, } for _, test := range tests { if test.wantErr == nil { test.wantErr = require.NoError } ns, err := buildGrypeNamespace(test.group) test.wantErr(t, err) if err != nil { return } assert.Equal(t, test.namespace, ns) } } func TestUnmarshalGitHubEntries(t *testing.T) { f, err := os.Open("testdata/github-github-python-0.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.GitHubAdvisoryEntries(f) require.NoError(t, err) assert.Len(t, entries, 2) } func TestParseGitHubEntry(t *testing.T) { expectedVulns := []db.Vulnerability{ { ID: "GHSA-p5wr-vp8g-q5p4", VersionConstraint: ">=4.0,<4.3.12", VersionFormat: "python", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2017-5524", Namespace: "nvd:cpe", }, }, PackageName: "plone", Namespace: "github:language:python", Fix: db.Fix{ State: db.FixedState, Versions: []string{"4.3.12"}, }, }, { ID: "GHSA-p5wr-vp8g-q5p4", VersionConstraint: ">=5.1a1,<5.1b1", VersionFormat: "python", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2017-5524", Namespace: "nvd:cpe", }, }, PackageName: "plone", Namespace: "github:language:python", Fix: db.Fix{ Versions: []string{"5.1b1"}, State: db.FixedState, }, }, { ID: "GHSA-p5wr-vp8g-q5p4", VersionConstraint: ">=5.0rc1,<5.0.7", VersionFormat: "python", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2017-5524", Namespace: "nvd:cpe", }, }, PackageName: "plone", Namespace: "github:language:python", Fix: db.Fix{ Versions: []string{"5.0.7"}, State: db.FixedState, }, }, } expectedMetadata := db.VulnerabilityMetadata{ ID: "GHSA-p5wr-vp8g-q5p4", Namespace: "github:language:python", RecordSource: "github:github:python", DataSource: "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", Severity: "Medium", URLs: []string{"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4"}, Description: "Moderate severity vulnerability that affects Plone", } f, err := os.Open("testdata/github-github-python-1.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.GitHubAdvisoryEntries(f) require.NoError(t, err) require.Len(t, entries, 1) entry := entries[0] dataEntries, err := Transform(entry) require.NoError(t, err) var vulns []db.Vulnerability for _, entry := range dataEntries { switch vuln := entry.Data.(type) { case db.Vulnerability: vulns = append(vulns, vuln) case db.VulnerabilityMetadata: assert.Equal(t, expectedMetadata, vuln) default: t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") } } // check vulnerability assert.Len(t, vulns, len(expectedVulns)) if diff := cmp.Diff(expectedVulns, vulns); diff != "" { t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) } } func TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) { expectedVuln := db.Vulnerability{ ID: "GHSA-vc9j-fhvv-8vrf", VersionConstraint: "<=0.2.0-prerelease.20200709173451", VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14000", Namespace: "nvd:cpe", }, }, PackageName: "scratch-vm", Namespace: "github:language:javascript", Fix: db.Fix{ Versions: []string{"0.2.0-prerelease.20200714185213"}, State: db.FixedState, }, } expectedMetadata := db.VulnerabilityMetadata{ ID: "GHSA-vc9j-fhvv-8vrf", Namespace: "github:language:javascript", RecordSource: "github:github:npm", DataSource: "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", Severity: "Critical", URLs: []string{"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf"}, Description: "Remote Code Execution in scratch-vm", Cvss: []db.Cvss{ { VendorMetadata: transformers.VendorBaseMetrics{ BaseSeverity: "Critical", Status: "N/A", }, Metrics: v5.NewCvssMetrics(9.8, 3.9, 5.9), Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", }, }, } f, err := os.Open("testdata/github-github-npm-0.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.GitHubAdvisoryEntries(f) require.NoError(t, err) require.Len(t, entries, 1) entry := entries[0] dataEntries, err := Transform(entry) assert.NoError(t, err) for _, entry := range dataEntries { switch vuln := entry.Data.(type) { case db.Vulnerability: assert.Equal(t, expectedVuln, vuln) case db.VulnerabilityMetadata: assert.Equal(t, expectedMetadata, vuln) default: t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") } } // check vulnerability assert.Len(t, dataEntries, 2) } func TestFilterWithdrawnEntries(t *testing.T) { f, err := os.Open("testdata/github-withdrawn.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.GitHubAdvisoryEntries(f) require.NoError(t, err) require.Len(t, entries, 1) entry := entries[0] dataEntries, err := Transform(entry) assert.NoError(t, err) assert.Nil(t, dataEntries) } ================================================ FILE: grype/db/v5/build/transformers/matchexclusions/transform.go ================================================ package matchexclusions import ( "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" db "github.com/anchore/grype/grype/db/v5" ) func Transform(matchExclusion unmarshal.MatchExclusion) ([]data.Entry, error) { exclusion := db.VulnerabilityMatchExclusion{ ID: matchExclusion.ID, Constraints: nil, Justification: matchExclusion.Justification, } for _, c := range matchExclusion.Constraints { constraint := &db.VulnerabilityMatchExclusionConstraint{ Vulnerability: db.VulnerabilityExclusionConstraint{ Namespace: c.Vulnerability.Namespace, FixState: db.FixState(c.Vulnerability.FixState), }, Package: db.PackageExclusionConstraint{ Name: c.Package.Name, Language: c.Package.Language, Type: c.Package.Type, Version: c.Package.Version, Location: c.Package.Location, }, } exclusion.Constraints = append(exclusion.Constraints, *constraint) } entries := []data.Entry{ { DBSchemaVersion: db.SchemaVersion, Data: exclusion, }, } return entries, nil } ================================================ FILE: grype/db/v5/build/transformers/msrc/testdata/microsoft-msrc-0.json ================================================ [ { "cvss": { "base_score": 7.8, "temporal_score": 7, "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4493470", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", "https://support.microsoft.com/help/4493470" ] }, { "id": "4494440", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", "https://support.microsoft.com/help/4494440" ] }, { "id": "4503267", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", "https://support.microsoft.com/en-us/help/4503267" ] }, { "id": "4507460", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", "https://support.microsoft.com/help/4507460" ] }, { "id": "4512517", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", "https://support.microsoft.com/help/4512517" ] }, { "id": "4516044", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", "https://support.microsoft.com/help/4516044" ] } ], "id": "CVE-2019-0671", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "High", "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", "vulnerable": [ "4480961", "4483229", "4487026", "4489882" ] }, { "cvss": { "base_score": 4.4, "temporal_score": 4, "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4093119", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" ] }, { "id": "4103723", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" ] }, { "id": "4284880", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" ] }, { "id": "4338814", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" ] }, { "id": "4343887", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" ] }, { "id": "4345418", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" ] }, { "id": "4457131", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" ] }, { "id": "4462917", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" ] }, { "id": "4467691", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" ] }, { "id": "4471321", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" ] } ], "id": "CVE-2018-8116", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "Medium", "summary": "Microsoft Graphics Component Denial of Service Vulnerability", "vulnerable": [ "3213986", "4013429", "4015217", "4019472", "4022715", "4025339", "4034658", "4038782", "4041691", "4048953", "4053579", "4056890", "4074590", "4088787" ] } ] ================================================ FILE: grype/db/v5/build/transformers/msrc/transform.go ================================================ package msrc import ( "fmt" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/build/transformers" "github.com/anchore/grype/grype/db/v5/namespace" "github.com/anchore/grype/grype/distro" ) // Transform gets called by the parser, which consumes entries from the JSON files previously pulled. Each VulnDBVulnerability represents // a single unmarshalled entry from the feed service func Transform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) { // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) recordSource := fmt.Sprintf("microsoft:msrc:%s", vulnerability.Product.ID) grypeNamespace, err := namespace.FromString(fmt.Sprintf("msrc:distro:%s:%s", distro.Windows, vulnerability.Product.ID)) if err != nil { return nil, err } entryNamespace := grypeNamespace.String() // In anchore-enterprise windows analyzer, "base" represents unpatched windows images (images with no KBs). // If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches), // then the image must be vulnerable to the image. //nolint:gocritic versionConstraint := append(vulnerability.Vulnerable, "base") allVulns := []db.Vulnerability{ { ID: vulnerability.ID, VersionConstraint: versionutil.OrConstraints(versionConstraint...), VersionFormat: "kb", PackageName: grypeNamespace.Resolver().Normalize(vulnerability.Product.ID), Namespace: entryNamespace, Fix: getFix(vulnerability), }, } // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) metadata := db.VulnerabilityMetadata{ ID: vulnerability.ID, DataSource: vulnerability.Link, Namespace: entryNamespace, RecordSource: recordSource, Severity: vulnerability.Severity, URLs: []string{vulnerability.Link}, // There is no description for vulnerabilities from the feed service // summary gives something like "windows information disclosure vulnerability" //Description: vulnerability.Summary, Cvss: []db.Cvss{ { Metrics: db.CvssMetrics{BaseScore: vulnerability.Cvss.BaseScore}, Vector: vulnerability.Cvss.Vector, }, }, } return transformers.NewEntries(allVulns, metadata), nil } func getFix(entry unmarshal.MSRCVulnerability) db.Fix { fixedInVersion := fixedInKB(entry) fixState := db.FixedState if fixedInVersion == "" { fixState = db.NotFixedState } return db.Fix{ Versions: []string{fixedInVersion}, State: fixState, } } // fixedInKB finds the "latest" patch (KB id) amongst the available microsoft patches and returns it // if the "latest" patch cannot be found, an error is returned func fixedInKB(vulnerability unmarshal.MSRCVulnerability) string { for _, fixedIn := range vulnerability.FixedIn { if fixedIn.IsLatest { return fixedIn.ID } } return "" } ================================================ FILE: grype/db/v5/build/transformers/msrc/transform_test.go ================================================ package msrc import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" db "github.com/anchore/grype/grype/db/v5" ) func TestUnmarshalMsrcVulnerabilities(t *testing.T) { f, err := os.Open("testdata/microsoft-msrc-0.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.MSRCVulnerabilityEntries(f) require.NoError(t, err) assert.Equal(t, len(entries), 2) } func TestParseMSRCEntry(t *testing.T) { expectedVulns := []struct { vulnerability db.Vulnerability metadata db.VulnerabilityMetadata }{ { vulnerability: db.Vulnerability{ ID: "CVE-2019-0671", VersionConstraint: `4480961 || 4483229 || 4487026 || 4489882 || base`, VersionFormat: "kb", PackageName: "10852", Namespace: "msrc:distro:windows:10852", Fix: db.Fix{ Versions: []string{"4516044"}, State: db.FixedState, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2019-0671", Severity: "High", DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671"}, Description: "", RecordSource: "microsoft:msrc:10852", Namespace: "msrc:distro:windows:10852", Cvss: []db.Cvss{ { Metrics: db.CvssMetrics{ BaseScore: 7.8, ImpactScore: nil, }, Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C", }, }, }, }, { vulnerability: db.Vulnerability{ ID: "CVE-2018-8116", VersionConstraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`, VersionFormat: "kb", PackageName: "10852", Namespace: "msrc:distro:windows:10852", Fix: db.Fix{ Versions: []string{"4345418"}, State: db.FixedState, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2018-8116", Namespace: "msrc:distro:windows:10852", DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", RecordSource: "microsoft:msrc:10852", Severity: "Medium", URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116"}, Description: "", Cvss: []db.Cvss{ { Metrics: db.CvssMetrics{ BaseScore: 4.4, ImpactScore: nil, }, Vector: "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C", }, }, }, }, } f, err := os.Open("testdata/microsoft-msrc-0.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.MSRCVulnerabilityEntries(f) require.NoError(t, err) assert.Equal(t, len(entries), 2) for idx, entry := range entries { dataEntries, err := Transform(entry) assert.NoError(t, err) assert.Len(t, dataEntries, 2) expected := expectedVulns[idx] for _, entry := range dataEntries { switch vuln := entry.Data.(type) { case db.Vulnerability: assert.Equal(t, expected.vulnerability, vuln) case db.VulnerabilityMetadata: assert.Equal(t, expected.metadata, vuln) default: t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") } } } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-first.json ================================================ { "cve": { "id": "CVE-2023-45283", "sourceIdentifier": "security@golang.org", "published": "2023-11-09T17:15:08.757", "lastModified": "2023-12-14T10:15:07.947", "vulnStatus": "Modified", "cveTags": [], "descriptions": [ { "lang": "en", "value": "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored." }, { "lang": "es", "value": "El paquete filepath no reconoce las rutas con el prefijo \\??\\ como especiales. En Windows, una ruta que comienza con \\??\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\?\\. Se pueden utilizar rutas con un prefijo \\??\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\??\\c:\\x es equivalente a la ruta más común c:\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\a\\..\\??\\b en la ruta raíz del dispositivo local \\??\\b. Clean ahora convertirá esto a .\\??\\b. De manera similar, Join(\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\??\\b. Unirse ahora convertirá esto a \\.\\??\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\??\\ como absolutas, y VolumeName informa correctamente el prefijo \\??\\ como nombre de volumen." } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-22" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionEndExcluding": "1.20.11", "matchCriteriaId": "C1E7C289-7484-4AA8-A96B-07D2E2933258" }, { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionStartIncluding": "1.21.0-0", "versionEndExcluding": "1.21.4", "matchCriteriaId": "4E3FC16C-41B2-4900-901F-48BDA3DC9ED2" } ] } ] } ], "references": [ { "url": "http://www.openwall.com/lists/oss-security/2023/12/05/2", "source": "security@golang.org" }, { "url": "https://go.dev/cl/540277", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/cl/541175", "source": "security@golang.org" }, { "url": "https://go.dev/issue/63713", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/issue/64028", "source": "security@golang.org" }, { "url": "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", "source": "security@golang.org", "tags": [ "Issue Tracking", "Mailing List", "Vendor Advisory" ] }, { "url": "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", "source": "security@golang.org" }, { "url": "https://pkg.go.dev/vuln/GO-2023-2185", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20231214-0008/", "source": "security@golang.org" } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-last.json ================================================ { "cve": { "id": "CVE-2023-45283", "sourceIdentifier": "security@golang.org", "published": "2023-11-09T17:15:08.757", "lastModified": "2023-12-14T10:15:07.947", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored." }, { "lang": "es", "value": "El paquete filepath no reconoce las rutas con el prefijo \\??\\ como especiales. En Windows, una ruta que comienza con \\??\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\?\\. Se pueden utilizar rutas con un prefijo \\??\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\??\\c:\\x es equivalente a la ruta más común c:\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\a\\..\\??\\b en la ruta raíz del dispositivo local \\??\\b. Clean ahora convertirá esto a .\\??\\b. De manera similar, Join(\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\??\\b. Unirse ahora convertirá esto a \\.\\??\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\??\\ como absolutas, y VolumeName informa correctamente el prefijo \\??\\ como nombre de volumen." } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-22" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionEndExcluding": "1.20.11", "matchCriteriaId": "C1E7C289-7484-4AA8-A96B-07D2E2933258" }, { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionStartIncluding": "1.21.0-0", "versionEndExcluding": "1.21.4", "matchCriteriaId": "4E3FC16C-41B2-4900-901F-48BDA3DC9ED2" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] } ] } ], "references": [ { "url": "http://www.openwall.com/lists/oss-security/2023/12/05/2", "source": "security@golang.org" }, { "url": "https://go.dev/cl/540277", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/cl/541175", "source": "security@golang.org" }, { "url": "https://go.dev/issue/63713", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/issue/64028", "source": "security@golang.org" }, { "url": "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", "source": "security@golang.org", "tags": [ "Issue Tracking", "Mailing List", "Vendor Advisory" ] }, { "url": "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", "source": "security@golang.org" }, { "url": "https://pkg.go.dev/vuln/GO-2023-2185", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20231214-0008/", "source": "security@golang.org" } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/compound-pkg.json ================================================ { "cve": { "id": "CVE-2018-10189", "sourceIdentifier": "cve@mitre.org", "published": "2018-04-17T20:29:00.410", "lastModified": "2018-05-23T14:41:49.073", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." }, { "lang": "es", "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 5.0 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 10.0, "impactScore": 2.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-200" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", "versionStartIncluding": "1.0.0", "versionEndIncluding": "1.4.1", "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" }, { "vulnerable": true, "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", "versionStartIncluding": "2.0.0", "versionEndExcluding": "2.13.0", "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" } ] } ] } ], "references": [ { "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", "source": "cve@mitre.org", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/cve-2020-10729.json ================================================ { "cve": { "id": "CVE-2020-10729", "sourceIdentifier": "secalert@redhat.com", "published": "2021-05-27T19:15:07.880", "lastModified": "2021-12-10T19:57:06.357", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6." }, { "lang": "es", "value": "Se encontró un fallo en el uso de valores insuficientemente aleatorios en Ansible. Dos búsquedas de contraseñas aleatorias de la misma longitud generan el mismo valor que la acción de almacenamiento en caché de la plantilla para el mismo archivo, ya que no se realiza una reevaluación. La mayor amenaza de esta vulnerabilidad sería que todas las contraseñas estén expuestas a la vez para el archivo. Este fallo afecta a Ansible Engine versiones anteriores a 2.9.6" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", "attackVector": "LOCAL", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 5.5, "baseSeverity": "MEDIUM" }, "exploitabilityScore": 1.8, "impactScore": 3.6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:L/AC:L/Au:N/C:P/I:N/A:N", "accessVector": "LOCAL", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 2.1 }, "baseSeverity": "LOW", "exploitabilityScore": 3.9, "impactScore": 2.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-330" } ] }, { "source": "secalert@redhat.com", "type": "Secondary", "description": [ { "lang": "en", "value": "CWE-330" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*", "versionEndExcluding": "2.9.6", "matchCriteriaId": "EDFA8005-6FBE-4032-A499-608B7FA34F56" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*", "matchCriteriaId": "142AD0DD-4CF3-4D74-9442-459CE3347E3A" }, { "vulnerable": false, "criteria": "cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*", "matchCriteriaId": "F4CFF558-3C47-480D-A2F0-BABF26042943" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", "matchCriteriaId": "07B237A9-69A3-4A9C-9DA0-4E06BD37AE73" } ] } ] } ], "references": [ { "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1831089", "source": "secalert@redhat.com", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://github.com/ansible/ansible/issues/34144", "source": "secalert@redhat.com", "tags": [ "Exploit", "Issue Tracking", "Third Party Advisory" ] }, { "url": "https://www.debian.org/security/2021/dsa-4950", "source": "secalert@redhat.com", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/cve-2022-0543.json ================================================ { "cve": { "id": "CVE-2022-0543", "sourceIdentifier": "security@debian.org", "published": "2022-02-18T20:15:17.583", "lastModified": "2023-09-29T15:55:24.533", "vulnStatus": "Analyzed", "cisaExploitAdd": "2022-03-28", "cisaActionDue": "2022-04-18", "cisaRequiredAction": "Apply updates per vendor instructions.", "cisaVulnerabilityName": "Debian-specific Redis Server Lua Sandbox Escape Vulnerability", "descriptions": [ { "lang": "en", "value": "It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution." }, { "lang": "es", "value": "Se ha detectado que redis, una base de datos persistente de valores clave, debido a un problema de empaquetado, es propenso a un escape del sandbox de Lua (específico de Debian), que podría resultar en una ejecución de código remota" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "CHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 10, "baseSeverity": "CRITICAL" }, "exploitabilityScore": 3.9, "impactScore": 6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:C/I:C/A:C", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "COMPLETE", "integrityImpact": "COMPLETE", "availabilityImpact": "COMPLETE", "baseScore": 10 }, "baseSeverity": "HIGH", "exploitabilityScore": 10, "impactScore": 10, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-862" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*", "matchCriteriaId": "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", "matchCriteriaId": "902B8056-9E37-443B-8905-8AA93E2447FB" }, { "vulnerable": false, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", "matchCriteriaId": "3D94DA3B-FA74-4526-A0A0-A872684598C6" }, { "vulnerable": false, "criteria": "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", "matchCriteriaId": "DEECE5FC-CACF-4496-A3E7-164736409252" }, { "vulnerable": false, "criteria": "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", "matchCriteriaId": "07B237A9-69A3-4A9C-9DA0-4E06BD37AE73" }, { "vulnerable": false, "criteria": "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", "matchCriteriaId": "FA6FEEC2-9F11-4643-8827-749718254FED" } ] } ] } ], "references": [ { "url": "http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html", "source": "security@debian.org", "tags": [ "Exploit", "Third Party Advisory", "VDB Entry" ] }, { "url": "https://bugs.debian.org/1005787", "source": "security@debian.org", "tags": [ "Issue Tracking", "Patch", "Third Party Advisory" ] }, { "url": "https://lists.debian.org/debian-security-announce/2022/msg00048.html", "source": "security@debian.org", "tags": [ "Mailing List", "Third Party Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20220331-0004/", "source": "security@debian.org", "tags": [ "Third Party Advisory" ] }, { "url": "https://www.debian.org/security/2022/dsa-5081", "source": "security@debian.org", "tags": [ "Mailing List", "Third Party Advisory" ] }, { "url": "https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce", "source": "security@debian.org", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/invalid_cpe.json ================================================ { "cve": { "id": "CVE-2015-8978", "sourceIdentifier": "cve@mitre.org", "published": "2016-11-22T17:59:00.180", "lastModified": "2016-11-28T19:50:59.600", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." }, { "lang": "es", "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "NONE", "integrityImpact": "NONE", "availabilityImpact": "HIGH", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "NONE", "integrityImpact": "NONE", "availabilityImpact": "PARTIAL", "baseScore": 5.0 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 10.0, "impactScore": 2.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-399" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", "versionEndIncluding": "1.14", "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" } ] } ] } ], "references": [ { "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", "source": "cve@mitre.org", "tags": [ "Vendor Advisory" ] }, { "url": "http://www.securityfocus.com/bid/94487", "source": "cve@mitre.org" } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/multiple-platforms-with-application-cpe.json ================================================ { "cve": { "id": "CVE-2023-38733", "sourceIdentifier": "psirt@us.ibm.com", "published": "2023-08-22T22:15:08.460", "lastModified": "2023-08-26T02:25:42.957", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "\nIBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs. IBM X-Force Id: 262293.\n\n" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 4.3, "baseSeverity": "MEDIUM" }, "exploitabilityScore": 2.8, "impactScore": 1.4 }, { "source": "psirt@us.ibm.com", "type": "Secondary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 4.3, "baseSeverity": "MEDIUM" }, "exploitabilityScore": 2.8, "impactScore": 1.4 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-532" } ] }, { "source": "psirt@us.ibm.com", "type": "Secondary", "description": [ { "lang": "en", "value": "CWE-532" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*", "versionStartIncluding": "21.0.0", "versionEndIncluding": "21.0.7.3", "matchCriteriaId": "DDF503DD-23DC-4B22-8873-BE94BF0F1CD1" }, { "vulnerable": true, "criteria": "cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*", "versionStartIncluding": "23.0.0", "versionEndIncluding": "23.0.3", "matchCriteriaId": "F513AA2B-F457-408B-8D5F-EBE657439000" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*", "matchCriteriaId": "F08E234C-BDCF-4B41-87B9-96BD5578CBBF" }, { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] } ] } ], "references": [ { "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/262293", "source": "psirt@us.ibm.com", "tags": [ "VDB Entry", "Vendor Advisory" ] }, { "url": "https://www.ibm.com/support/pages/node/7028223", "source": "psirt@us.ibm.com", "tags": [ "Patch", "Vendor Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/platform-cpe.json ================================================ { "cve": { "id": "CVE-2022-26488", "sourceIdentifier": "cve@mitre.org", "published": "2022-03-10T17:47:45.383", "lastModified": "2022-09-03T03:34:19.933", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2." }, { "lang": "es", "value": "En Python versiones anteriores a 3.10.3 en Windows, los usuarios locales pueden alcanzar privilegios porque la ruta de búsqueda no está asegurada apropiadamente. El instalador puede permitir a un atacante local añadir directorios escribibles por el usuario a la ruta de búsqueda del sistema. Para explotarla, un administrador debe haber instalado Python para todos los usuarios y habilitar las entradas PATH. Un usuario no administrador puede desencadenar una reparación que añada incorrectamente rutas escribibles por el usuario en el PATH, permitiendo el secuestro de la ruta de búsqueda de otros usuarios y servicios del sistema. Esto afecta a Python (CPython) versiones hasta 3.7.12, versiones 3.8.x hasta 3.8.12, versiones 3.9.x hasta 3.9.10, y versiones 3.10.x hasta 3.10.2" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", "attackVector": "LOCAL", "attackComplexity": "HIGH", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 7, "baseSeverity": "HIGH" }, "exploitabilityScore": 1, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:L/AC:M/Au:N/C:P/I:P/A:P", "accessVector": "LOCAL", "accessComplexity": "MEDIUM", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 4.4 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 3.4, "impactScore": 6.4, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-426" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionEndIncluding": "3.7.12", "matchCriteriaId": "1E05F88A-70C2-4DB6-9CCC-1D599AD26D4C" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionStartIncluding": "3.8.0", "versionEndIncluding": "3.8.12", "matchCriteriaId": "E80CA0FB-E708-4E92-BF36-7267F799FF8D" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionStartIncluding": "3.9.0", "versionEndIncluding": "3.9.10", "matchCriteriaId": "DD4B9F29-F505-4721-A630-C75103942F29" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionStartIncluding": "3.10.0", "versionEndIncluding": "3.10.2", "matchCriteriaId": "D5B55D1D-031C-4006-A368-BB66C2057916" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha1:*:*:*:*:*:*", "matchCriteriaId": "514A577E-5E60-40BA-ABD0-A8C5EB28BD90" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha2:*:*:*:*:*:*", "matchCriteriaId": "83B71795-9C81-4E5F-967C-C11808F24B05" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha3:*:*:*:*:*:*", "matchCriteriaId": "3F6F71F3-299E-4A4B-ADD1-EAD5A1D433E2" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*", "matchCriteriaId": "09BBF4E9-EA54-41B5-948E-8E3D2660B7EF" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*", "matchCriteriaId": "D9BBF4E9-EA54-41B5-948E-8E3D2660B7EF" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha5:*:*:*:*:*:*", "matchCriteriaId": "AEBFDCE7-81D4-4741-BB88-12C704515F5C" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha6:*:*:*:*:*:*", "matchCriteriaId": "156EB4C2-EFB7-4CEB-804D-93DB62992A63" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] } ] }, { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:active_iq_unified_manager:-:*:*:*:*:windows:*:*", "matchCriteriaId": "B55E8D50-99B4-47EC-86F9-699B67D473CE" }, { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:ontap_select_deploy_administration_utility:-:*:*:*:*:*:*:*", "matchCriteriaId": "E7CF3019-975D-40BB-A8A4-894E62BD3797" } ] } ] } ], "references": [ { "url": "https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/", "source": "cve@mitre.org", "tags": [ "Patch", "Vendor Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20220419-0005/", "source": "cve@mitre.org", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/single-package-multi-distro.json ================================================ { "cve": { "id": "CVE-2018-1000222", "sourceIdentifier": "cve@mitre.org", "published": "2018-08-20T20:29:01.347", "lastModified": "2020-03-31T02:15:12.667", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." }, { "lang": "es", "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "REQUIRED", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 8.8, "baseSeverity": "HIGH" }, "exploitabilityScore": 2.8, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "MEDIUM", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 6.8 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 8.6, "impactScore": 6.4, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": true } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-415" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" }, { "vulnerable": true, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" }, { "vulnerable": true, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" } ] } ] } ], "references": [ { "url": "https://github.com/libgd/libgd/issues/447", "source": "cve@mitre.org", "tags": [ "Issue Tracking", "Third Party Advisory" ] }, { "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "source": "cve@mitre.org", "tags": [ "Mailing List", "Third Party Advisory" ] }, { "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "source": "cve@mitre.org" }, { "url": "https://security.gentoo.org/glsa/201903-18", "source": "cve@mitre.org", "tags": [ "Third Party Advisory" ] }, { "url": "https://usn.ubuntu.com/3755-1/", "source": "cve@mitre.org", "tags": [ "Mitigation", "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/unmarshal-test.json ================================================ { "cve": { "id": "CVE-2003-0349", "sourceIdentifier": "cve@mitre.org", "published": "2003-07-24T04:00:00.000", "lastModified": "2018-10-12T21:32:41.083", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "Buffer overflow in the streaming media component for logging multicast requests in the ISAPI for the logging capability of Microsoft Windows Media Services (nsiislog.dll), as installed in IIS 5.0, allows remote attackers to execute arbitrary code via a large POST request to nsiislog.dll." }, { "lang": "es", "value": "Desbordamiento de búfer en el componente de secuenciamiento (streaming) de medios para registrar peticiones de multidifusión en la librería ISAPI de la capacidad de registro (logging) de Microsoft Windows Media Services (nsiislog.dll), como el instalado en IIS 5.9, permite a atacantes remotos ejecutar código arbitrario mediante una petición POST larga a nsiislog.dll." } ], "metrics": { "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 7.5 }, "baseSeverity": "HIGH", "exploitabilityScore": 10.0, "impactScore": 6.4, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": true, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "NVD-CWE-Other" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*", "matchCriteriaId": "4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD" } ] } ] } ], "references": [ { "url": "http://marc.info/?l=bugtraq&m=105665030925504&w=2", "source": "cve@mitre.org" }, { "url": "http://securitytracker.com/id?1007059", "source": "cve@mitre.org" }, { "url": "http://www.kb.cert.org/vuls/id/113716", "source": "cve@mitre.org", "tags": [ "US Government Resource" ] }, { "url": "http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563", "source": "cve@mitre.org", "tags": [ "Exploit", "Patch", "Vendor Advisory" ] }, { "url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022", "source": "cve@mitre.org" }, { "url": "https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938", "source": "cve@mitre.org" } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/testdata/version-range.json ================================================ { "cve": { "id": "CVE-2018-5487", "sourceIdentifier": "security-alert@netapp.com", "published": "2018-05-24T14:29:00.390", "lastModified": "2018-07-05T13:52:30.627", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." }, { "lang": "es", "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 9.8, "baseSeverity": "CRITICAL" }, "exploitabilityScore": 3.9, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 7.5 }, "baseSeverity": "HIGH", "exploitabilityScore": 10.0, "impactScore": 6.4, "acInsufInfo": true, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-20" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", "versionStartIncluding": "7.2", "versionEndIncluding": "7.3", "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" } ] } ] } ], "references": [ { "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", "source": "security-alert@netapp.com", "tags": [ "Patch", "Vendor Advisory" ] } ] } } ================================================ FILE: grype/db/v5/build/transformers/nvd/transform.go ================================================ package nvd import ( "slices" "sort" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/build/transformers" "github.com/anchore/grype/grype/db/v5/namespace" "github.com/anchore/grype/grype/db/v5/pkg/qualifier" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/syft/syft/cpe" ) type Config struct { CPEParts *strset.Set InferNVDFixVersions bool } func defaultConfig() Config { return Config{ CPEParts: strset.New("a"), InferNVDFixVersions: true, } } func Transformer(cfg Config) data.NVDTransformer { if cfg == (Config{}) { cfg = defaultConfig() } return func(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { return transform(cfg, vulnerability) } } func transform(cfg Config, vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) recordSource := "nvdv2:nvdv2:cves" grypeNamespace, err := namespace.FromString("nvd:cpe") if err != nil { return nil, err } entryNamespace := grypeNamespace.String() uniquePkgs := findUniquePkgs(cfg, vulnerability.Configurations...) // extract all links var links []string for _, externalRefs := range vulnerability.References { // TODO: should we capture other information here? if externalRefs.URL != "" { links = append(links, externalRefs.URL) } } // duplicate the vulnerabilities based on the set of unique packages the vulnerability is for var allVulns []db.Vulnerability for _, p := range uniquePkgs.All() { var qualifiers []qualifier.Qualifier matches := uniquePkgs.Matches(p) cpes := strset.New() for _, m := range matches { cpes.Add(grypeNamespace.Resolver().Normalize(m.Criteria)) } if p.PlatformCPE != "" { qualifiers = []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: p.PlatformCPE, }} } orderedCPEs := cpes.List() sort.Strings(orderedCPEs) // create vulnerability entry allVulns = append(allVulns, db.Vulnerability{ ID: vulnerability.ID, PackageQualifiers: qualifiers, VersionConstraint: buildConstraints(matches), VersionFormat: strings.ToLower(getVersionFormat(p.Product, orderedCPEs).String()), PackageName: grypeNamespace.Resolver().Normalize(p.Product), Namespace: entryNamespace, CPEs: orderedCPEs, Fix: getFix(matches, cfg.InferNVDFixVersions), }) } // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) allCVSS := vulnerability.CVSS() metadata := db.VulnerabilityMetadata{ ID: vulnerability.ID, DataSource: "https://nvd.nist.gov/vuln/detail/" + vulnerability.ID, Namespace: entryNamespace, RecordSource: recordSource, Severity: nvd.CvssSummaries(allCVSS).Sorted().Severity(), URLs: links, Description: vulnerability.Description(), Cvss: getCvss(allCVSS...), } return transformers.NewEntries(allVulns, metadata), nil } func getVersionFormat(name string, cpes []string) version.Format { if pkg.HasJvmPackageName(name) { return version.JVMFormat } for _, c := range cpes { att, err := cpe.NewAttributes(c) if err != nil { continue } if pkg.HasJvmPackageName(att.Product) { return version.JVMFormat } } return version.UnknownFormat } func getFix(matches []nvd.CpeMatch, inferNVDFixVersions bool) db.Fix { if !inferNVDFixVersions { return db.Fix{ State: db.UnknownFixState, } } possiblyFixed := strset.New() knownAffected := strset.New() unspecifiedSet := strset.New("*", "-", "*") for _, match := range matches { if !match.Vulnerable { continue } if match.VersionEndExcluding != nil && !unspecifiedSet.Has(*match.VersionEndExcluding) { possiblyFixed.Add(*match.VersionEndExcluding) } if match.VersionStartIncluding != nil && !unspecifiedSet.Has(*match.VersionStartIncluding) { knownAffected.Add(*match.VersionStartIncluding) } if match.VersionEndIncluding != nil && !unspecifiedSet.Has(*match.VersionEndIncluding) { knownAffected.Add(*match.VersionEndIncluding) } matchCPE, err := cpe.New(match.Criteria, cpe.DeclaredSource) if err != nil { continue } if !unspecifiedSet.Has(matchCPE.Attributes.Version) { knownAffected.Add(matchCPE.Attributes.Version) } } possiblyFixed.Remove(knownAffected.List()...) var fixes []string fixState := db.UnknownFixState if possiblyFixed.Size() > 0 { fixState = db.FixedState fixes = possiblyFixed.List() slices.Sort(fixes) } return db.Fix{ Versions: fixes, State: fixState, } } func getCvss(cvss ...nvd.CvssSummary) []db.Cvss { var results []db.Cvss for _, c := range cvss { results = append(results, db.Cvss{ Source: c.Source, Type: string(c.Type), Version: c.Version, Vector: c.Vector, Metrics: db.CvssMetrics{ BaseScore: c.BaseScore, ExploitabilityScore: c.ExploitabilityScore, ImpactScore: c.ImpactScore, }, }) } return results } ================================================ FILE: grype/db/v5/build/transformers/nvd/transform_test.go ================================================ package nvd import ( "os" "testing" "github.com/go-test/deep" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/grype/grype/db/internal/testutil" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/pkg/qualifier" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe" "github.com/anchore/grype/grype/version" ) func TestUnmarshalNVDVulnerabilitiesEntries(t *testing.T) { f, err := os.Open("testdata/unmarshal-test.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.NvdVulnerabilityEntries(f) assert.NoError(t, err) assert.Len(t, entries, 1) } func TestParseAllNVDVulnerabilityEntries(t *testing.T) { tests := []struct { name string config Config numEntries int fixture string vulns []db.Vulnerability metadata db.VulnerabilityMetadata }{ { name: "AppVersionRange", numEntries: 1, fixture: "testdata/version-range.json", vulns: []db.Vulnerability{ { ID: "CVE-2018-5487", PackageName: "oncommand_unified_manager", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", }}, VersionConstraint: ">= 7.2, <= 7.3", VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*"}, Fix: db.Fix{ State: "unknown", }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2018-5487", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", Namespace: "nvd:cpe", RecordSource: "nvdv2:nvdv2:cves", Severity: "Critical", URLs: []string{"https://security.netapp.com/advisory/ntap-20180523-0001/"}, Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", Cvss: []db.Cvss{ { Metrics: db.NewCvssMetrics( 7.5, 10, 6.4, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { Metrics: db.NewCvssMetrics( 9.8, 3.9, 5.9, ), Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.0", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "App+OS", numEntries: 1, fixture: "testdata/single-package-multi-distro.json", vulns: []db.Vulnerability{ { ID: "CVE-2018-1000222", PackageName: "libgd", VersionConstraint: "= 2.2.5", VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*"}, Fix: db.Fix{ State: "unknown", }, }, // TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong! }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2018-1000222", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000222", Namespace: "nvd:cpe", RecordSource: "nvdv2:nvdv2:cves", Severity: "High", URLs: []string{"https://github.com/libgd/libgd/issues/447", "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "https://security.gentoo.org/glsa/201903-18", "https://usn.ubuntu.com/3755-1/"}, Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", Cvss: []db.Cvss{ { Metrics: db.NewCvssMetrics( 6.8, 8.6, 6.4, ), Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { Metrics: db.NewCvssMetrics( 8.8, 2.8, 5.9, ), Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", Version: "3.0", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "AppCompoundVersionRange", numEntries: 1, fixture: "testdata/compound-pkg.json", vulns: []db.Vulnerability{ { ID: "CVE-2018-10189", PackageName: "mautic", VersionConstraint: ">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0", VersionFormat: "unknown", Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*"}, // note: entry was dedupicated Fix: db.Fix{ Versions: []string{"2.13.0"}, State: "fixed", }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2018-10189", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-10189", Namespace: "nvd:cpe", RecordSource: "nvdv2:nvdv2:cves", Severity: "High", URLs: []string{"https://github.com/mautic/mautic/releases/tag/2.13.0"}, Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", Cvss: []db.Cvss{ { Metrics: db.NewCvssMetrics( 5, 10, 2.9, ), Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { Metrics: db.NewCvssMetrics( 7.5, 3.9, 3.6, ), Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", Version: "3.0", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { // we always keep the metadata even though there are no vulnerability entries for it name: "InvalidCPE", numEntries: 1, fixture: "testdata/invalid_cpe.json", vulns: nil, metadata: db.VulnerabilityMetadata{ ID: "CVE-2015-8978", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2015-8978", RecordSource: "nvdv2:nvdv2:cves", Severity: "High", URLs: []string{ "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", "http://www.securityfocus.com/bid/94487", }, Description: "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML.", Cvss: []db.Cvss{ { Metrics: db.NewCvssMetrics( 5, 10, 2.9, ), Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { Metrics: db.NewCvssMetrics( 7.5, 3.9, 3.6, ), Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", Version: "3.0", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "With Platform CPE", numEntries: 1, fixture: "testdata/platform-cpe.json", vulns: []db.Vulnerability{ { ID: "CVE-2022-26488", PackageName: "active_iq_unified_manager", VersionConstraint: "", VersionFormat: "unknown", Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:a:netapp:active_iq_unified_manager:-:*:*:*:*:windows:*:*"}, Fix: db.Fix{ State: "unknown", }, }, { ID: "CVE-2022-26488", PackageName: "ontap_select_deploy_administration_utility", VersionConstraint: "", VersionFormat: "unknown", Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:a:netapp:ontap_select_deploy_administration_utility:-:*:*:*:*:*:*:*"}, Fix: db.Fix{ State: "unknown", }, }, { ID: "CVE-2022-26488", PackageName: "python", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }}, VersionConstraint: "<= 3.7.12 || >= 3.8.0, <= 3.8.12 || >= 3.9.0, <= 3.9.10 || >= 3.10.0, <= 3.10.2 || = 3.11.0-alpha1 || = 3.11.0-alpha2 || = 3.11.0-alpha3 || = 3.11.0-alpha4 || = 3.11.0-alpha5 || = 3.11.0-alpha6", VersionFormat: "unknown", Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "cpe:2.3:a:python:python:3.11.0:alpha1:*:*:*:*:*:*", "cpe:2.3:a:python:python:3.11.0:alpha2:*:*:*:*:*:*", "cpe:2.3:a:python:python:3.11.0:alpha3:*:*:*:*:*:*", "cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*", "cpe:2.3:a:python:python:3.11.0:alpha5:*:*:*:*:*:*", "cpe:2.3:a:python:python:3.11.0:alpha6:*:*:*:*:*:*", }, Fix: db.Fix{ State: "unknown", }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2022-26488", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2022-26488", RecordSource: "nvdv2:nvdv2:cves", Severity: "High", URLs: []string{ "https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/", "https://security.netapp.com/advisory/ntap-20220419-0005/", }, Description: "In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2.", Cvss: []db.Cvss{ { Metrics: db.NewCvssMetrics( 4.4, 3.4, 6.4, ), Vector: "AV:L/AC:M/Au:N/C:P/I:P/A:P", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { Metrics: db.NewCvssMetrics( 7, 1, 5.9, ), Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "CVE-2022-0543 multiple platforms", numEntries: 1, fixture: "testdata/cve-2022-0543.json", vulns: []db.Vulnerability{ { ID: "CVE-2022-0543", PackageName: "redis", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", }}, VersionConstraint: "", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{State: "unknown"}, Advisories: nil, }, { ID: "CVE-2022-0543", PackageName: "redis", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", }}, VersionConstraint: "", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{State: "unknown"}, Advisories: nil, }, { ID: "CVE-2022-0543", PackageName: "redis", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", }}, VersionConstraint: "", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{State: "unknown"}, Advisories: nil, }, { ID: "CVE-2022-0543", PackageName: "redis", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", }}, VersionConstraint: "", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{State: "unknown"}, Advisories: nil, }, { ID: "CVE-2022-0543", PackageName: "redis", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", }}, VersionConstraint: "", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{State: "unknown"}, Advisories: nil, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2022-0543", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2022-0543", RecordSource: "nvdv2:nvdv2:cves", Severity: "Critical", URLs: []string{ "http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html", "https://bugs.debian.org/1005787", "https://lists.debian.org/debian-security-announce/2022/msg00048.html", "https://security.netapp.com/advisory/ntap-20220331-0004/", "https://www.debian.org/security/2022/dsa-5081", "https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce", }, Description: "It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution.", Cvss: []db.Cvss{ { VendorMetadata: nil, Metrics: db.NewCvssMetrics(10, 10, 10), Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { VendorMetadata: nil, Metrics: db.NewCvssMetrics(10, 3.9, 6), Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", Version: "3.1", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "CVE-2020-10729 multiple platforms omitted top level config", numEntries: 1, fixture: "testdata/cve-2020-10729.json", vulns: []db.Vulnerability{ { ID: "CVE-2020-10729", PackageName: "ansible_engine", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*", }}, VersionConstraint: "< 2.9.6", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{ Versions: []string{"2.9.6"}, State: "fixed", }, Advisories: nil, }, { ID: "CVE-2020-10729", PackageName: "ansible_engine", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*", }}, VersionConstraint: "< 2.9.6", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{ Versions: []string{"2.9.6"}, State: "fixed", }, Advisories: nil, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2020-10729", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2020-10729", RecordSource: "nvdv2:nvdv2:cves", Severity: "Medium", URLs: []string{ "https://bugzilla.redhat.com/show_bug.cgi?id=1831089", "https://github.com/ansible/ansible/issues/34144", "https://www.debian.org/security/2021/dsa-4950", }, Description: "A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6.", Cvss: []db.Cvss{ { VendorMetadata: nil, Metrics: db.NewCvssMetrics( 2.1, 3.9, 2.9, ), Vector: "AV:L/AC:L/Au:N/C:P/I:N/A:N", Version: "2.0", Source: "nvd@nist.gov", Type: "Primary", }, { VendorMetadata: nil, Metrics: db.NewCvssMetrics( 5.5, 1.8, 3.6, ), Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", Version: "3.1", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "multiple platforms some are application", numEntries: 2, fixture: "testdata/multiple-platforms-with-application-cpe.json", vulns: []db.Vulnerability{ { ID: "CVE-2023-38733", PackageName: "robotic_process_automation", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*", }}, VersionConstraint: ">= 21.0.0, <= 21.0.7.3 || >= 23.0.0, <= 23.0.3", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{ State: "unknown", }, Advisories: nil, }, { ID: "CVE-2023-38733", PackageName: "robotic_process_automation", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }}, VersionConstraint: ">= 21.0.0, <= 21.0.7.3 || >= 23.0.0, <= 23.0.3", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{ State: "unknown", }, Advisories: nil, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2023-38733", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2023-38733", RecordSource: "nvdv2:nvdv2:cves", Severity: "Medium", URLs: []string{ "https://exchange.xforce.ibmcloud.com/vulnerabilities/262293", "https://www.ibm.com/support/pages/node/7028223", }, Description: "\nIBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs. IBM X-Force Id: 262293.\n\n", Cvss: []db.Cvss{ { VendorMetadata: nil, Metrics: db.NewCvssMetrics(4.3, 2.8, 1.4), Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", Version: "3.1", Source: "nvd@nist.gov", Type: "Primary", }, { VendorMetadata: nil, Metrics: db.NewCvssMetrics(4.3, 2.8, 1.4), Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", Version: "3.1", Source: "psirt@us.ibm.com", Type: "Secondary", }, }, }, }, { name: "Platform CPE first in CPE config list", numEntries: 1, fixture: "testdata/CVE-2023-45283-platform-cpe-first.json", vulns: []db.Vulnerability{ { ID: "CVE-2023-45283", PackageName: "go", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }}, VersionConstraint: "< 1.20.11 || >= 1.21.0-0, < 1.21.4", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{ Versions: []string{"1.20.11", "1.21.4"}, State: "fixed", }, Advisories: nil, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2023-45283", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2023-45283", RecordSource: "nvdv2:nvdv2:cves", Severity: "High", URLs: []string{ "http://www.openwall.com/lists/oss-security/2023/12/05/2", "https://go.dev/cl/540277", "https://go.dev/cl/541175", "https://go.dev/issue/63713", "https://go.dev/issue/64028", "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", "https://pkg.go.dev/vuln/GO-2023-2185", "https://security.netapp.com/advisory/ntap-20231214-0008/", }, Description: "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored.", Cvss: []db.Cvss{ { VendorMetadata: nil, Metrics: db.NewCvssMetrics(7.5, 3.9, 3.6), Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", Version: "3.1", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, { name: "Platform CPE last in CPE config list", numEntries: 1, fixture: "testdata/CVE-2023-45283-platform-cpe-last.json", vulns: []db.Vulnerability{ { ID: "CVE-2023-45283", PackageName: "go", Namespace: "nvd:cpe", PackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }}, VersionConstraint: "< 1.20.11 || >= 1.21.0-0, < 1.21.4", VersionFormat: "unknown", CPEs: []string{"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*"}, RelatedVulnerabilities: nil, Fix: db.Fix{ Versions: []string{"1.20.11", "1.21.4"}, State: "fixed", }, Advisories: nil, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2023-45283", Namespace: "nvd:cpe", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2023-45283", RecordSource: "nvdv2:nvdv2:cves", Severity: "High", URLs: []string{ "http://www.openwall.com/lists/oss-security/2023/12/05/2", "https://go.dev/cl/540277", "https://go.dev/cl/541175", "https://go.dev/issue/63713", "https://go.dev/issue/64028", "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", "https://pkg.go.dev/vuln/GO-2023-2185", "https://security.netapp.com/advisory/ntap-20231214-0008/", }, Description: "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored.", Cvss: []db.Cvss{ { VendorMetadata: nil, Metrics: db.NewCvssMetrics(7.5, 3.9, 3.6), Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", Version: "3.1", Source: "nvd@nist.gov", Type: "Primary", }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.config == (Config{}) { test.config = defaultConfig() } f, err := os.Open(test.fixture) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, f.Close()) }) entries, err := unmarshal.NvdVulnerabilityEntries(f) require.NoError(t, err) var vulns []db.Vulnerability for _, entry := range entries { dataEntries, err := transform(test.config, entry.Cve) require.NoError(t, err) for _, entry := range dataEntries { switch vuln := entry.Data.(type) { case db.Vulnerability: vulns = append(vulns, vuln) case db.VulnerabilityMetadata: // check metadata if diff := deep.Equal(test.metadata, vuln); diff != nil { for _, d := range diff { t.Errorf("metadata diff: %+v", d) } } default: t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") } } } if diff := cmp.Diff(test.vulns, vulns); diff != "" { t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) } }) } } func TestGetVersionFormat(t *testing.T) { tests := []struct { name string input string cpes []string expected version.Format }{ { name: "detects JVM format from name", input: "java_se", cpes: []string{}, expected: version.JVMFormat, }, { name: "detects JVM format from CPEs", input: "other_product", cpes: []string{"cpe:2.3:a:oracle:openjdk:11:update53:*:*:*:*:*:*"}, expected: version.JVMFormat, }, { name: "detects JVM format from another CPE (zulu)", input: "other_product", cpes: []string{"cpe:2.3:a:zula:zulu:15:*:*:*:*:*:*:*"}, expected: version.JVMFormat, }, { name: "detects JVM format from another CPE (jdk)", input: "other_product", cpes: []string{"cpe:2.3:a:oracle:jdk:11.0:*:*:*:*:*:*:*"}, expected: version.JVMFormat, }, { name: "detects JVM format from another CPE (jre)", input: "other_product", cpes: []string{"cpe:2.3:a:oracle:jre:11.0:*:*:*:*:*:*:*"}, expected: version.JVMFormat, }, { name: "returns unknown format for non-JVM product and non-JVM CPEs", input: "non_jvm_product", cpes: []string{"cpe:2.3:a:some_other_product:product_name:1.0:*:*:*:*:*:*"}, expected: version.UnknownFormat, }, { name: "handles invalid CPE gracefully", input: "non_jvm_product", cpes: []string{"invalid_cpe_format"}, expected: version.UnknownFormat, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { format := getVersionFormat(tt.input, tt.cpes) assert.Equal(t, tt.expected, format) }) } } func TestGetFix(t *testing.T) { tests := []struct { name string matches []nvd.CpeMatch expected db.Fix }{ { name: "Equals", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "VersionEndExcluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionEndExcluding: strRef("2.3.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: []string{"2.3.0"}, State: "fixed", }, }, { name: "VersionEndIncluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionEndIncluding: strRef("2.3.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "VersionStartExcluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartExcluding: strRef("2.3.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "VersionStartIncluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "Version Range", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndIncluding: strRef("2.5.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "Multiple Version Ranges", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndIncluding: strRef("2.5.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartExcluding: strRef("3.3.0"), VersionEndExcluding: strRef("3.5.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: []string{"3.5.0"}, State: "fixed", }, }, { name: "Empty end exclude treated as unknown", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartExcluding: strRef("3.3.0"), VersionEndExcluding: strRef(""), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "Multiple fixes with deduplication", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("3.3.0"), VersionEndExcluding: strRef("3.5.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("0"), VersionEndExcluding: strRef("1.7.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target-2:*:*", VersionStartIncluding: strRef("0"), VersionEndExcluding: strRef("1.7.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: []string{"1.7.0", "3.5.0"}, State: "fixed", }, }, { name: "< version as end in a separate affected >= range", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndExcluding: strRef("2.5.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.5.0"), VersionEndExcluding: strRef("3.5.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: []string{"3.5.0"}, State: "fixed", }, }, { name: "< version as start in a separate affected <= range", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndExcluding: strRef("2.5.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.1.0"), VersionEndIncluding: strRef("2.5.0"), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "< range with same version affected == critera", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndExcluding: strRef("2.5.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:2.5.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, { name: "< range with another unaffected entry", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndExcluding: strRef("2.5.0"), Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:2.5.0:*:*:*:*:target:*:*", Vulnerable: false, }, }, expected: db.Fix{ Versions: []string{"2.5.0"}, State: "fixed", }, }, { name: "treat * in < as unknown fix state", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndExcluding: strRef("*"), Vulnerable: true, }, }, expected: db.Fix{ Versions: nil, State: "unknown", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fix := getFix(tt.matches, true) assert.Equal(t, tt.expected, fix) }) t.Run(tt.name+" don't infer NVD fixes", func(t *testing.T) { fix := getFix(tt.matches, false) assert.Equal(t, db.Fix{ Versions: nil, State: "unknown", }, fix) }) } } ================================================ FILE: grype/db/v5/build/transformers/nvd/unique_pkg.go ================================================ package nvd import ( "fmt" "strings" "github.com/umisama/go-cpe" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/grype/grype/db/internal/versionutil" "github.com/anchore/grype/internal/log" ) const ( ANY = "*" NA = "-" ) type pkgCandidate struct { Product string Vendor string TargetSoftware string PlatformCPE string } func (p pkgCandidate) String() string { if p.PlatformCPE == "" { return fmt.Sprintf("%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware) } return fmt.Sprintf("%s|%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware, p.PlatformCPE) } func newPkgCandidate(tCfg Config, match nvd.CpeMatch, platformCPE string) (*pkgCandidate, error) { // we are only interested in packages that are vulnerable (not related to secondary match conditioning) if !match.Vulnerable { return nil, nil } c, err := cpe.NewItemFromFormattedString(match.Criteria) if err != nil { return nil, fmt.Errorf("unable to create uniquePkgEntry from '%s': %w", match.Criteria, err) } // we are interested in applications, conditionally operating systems, but never hardware part := c.Part() if !tCfg.CPEParts.Has(string(part)) { return nil, nil } return &pkgCandidate{ Product: c.Product().String(), Vendor: c.Vendor().String(), TargetSoftware: c.TargetSw().String(), PlatformCPE: platformCPE, }, nil } func findUniquePkgs(tCfg Config, cfgs ...nvd.Configuration) uniquePkgTracker { set := newUniquePkgTracker() for _, c := range cfgs { _findUniquePkgs(tCfg, set, c) } return set } func platformPackageCandidates(tCfg Config, set uniquePkgTracker, c nvd.Configuration) bool { nodes := c.Nodes /* Turn a configuration like this: (AND (OR (cpe:2.3:a:redis:...whatever) (cpe:2.3.:something:...whatever) (OR (cpe:2.3:o:debian:9....) (cpe:2.3:o:ubuntu:22..)) ) Into a configuration like this: (OR (AND (cpe:2.3:a:redis:...whatever) (cpe:2.3:o:debian:9...)) (AND (cpe:2.3:a:redis:...whatever) (cpe:2.3:o:ubuntu:22...)) (AND (cpe:2.3:a:something:...whatever) (cpe:2.3:o:debian:9...)) (AND (cpe:2.3:a:something:...whatever) (cpe:2.3:o:ubuntu:22...)) ) Because in schema v5, rows in Grype DB can only have zero or one platform CPE constraint. */ if len(nodes) != 2 || c.Operator == nil || *c.Operator != nvd.And { return false } var platformsNode nvd.Node var applicationNode nvd.Node for _, n := range nodes { if anyHardwareCPEPresent(n) { return false } if allCPEsVulnerable(n) { applicationNode = n } if noCPEsVulnerable(n) { platformsNode = n } } if platformsNode.Operator != nvd.Or { return false } if applicationNode.Operator != nvd.Or { return false } result := false matches := platformsNode.CpeMatch for _, application := range applicationNode.CpeMatch { for _, maybePlatform := range matches { platform := maybePlatform.Criteria candidate, err := newPkgCandidate(tCfg, application, platform) if err != nil { log.Debugf("unable processing uniquePkg with multiple platforms: %v", err) continue } if candidate == nil { continue } set.Add(*candidate, application) result = true } } return result } func anyHardwareCPEPresent(n nvd.Node) bool { for _, c := range n.CpeMatch { parts := strings.Split(c.Criteria, ":") if len(parts) < 3 || parts[2] == "h" { return true } } return false } func allCPEsVulnerable(node nvd.Node) bool { for _, c := range node.CpeMatch { if !c.Vulnerable { return false } } return true } func noCPEsVulnerable(node nvd.Node) bool { for _, c := range node.CpeMatch { if c.Vulnerable { return false } } return true } func _findUniquePkgs(tCfg Config, set uniquePkgTracker, c nvd.Configuration) { if len(c.Nodes) == 0 { return } if platformPackageCandidates(tCfg, set, c) { return } for _, node := range c.Nodes { for _, match := range node.CpeMatch { candidate, err := newPkgCandidate(tCfg, match, "") if err != nil { // Do not halt all execution because of being unable to create // a PkgCandidate. This can happen when a CPE is invalid which // could avoid creating a database log.Debugf("unable processing uniquePkg: %v", err) continue } if candidate != nil { set.Add(*candidate, match) } } } } func buildConstraints(matches []nvd.CpeMatch) string { constraints := make([]string, 0) for _, match := range matches { constraints = append(constraints, buildConstraint(match)) } return versionutil.OrConstraints(removeDuplicateConstraints(constraints)...) } func buildConstraint(match nvd.CpeMatch) string { constraints := make([]string, 0) if match.VersionStartIncluding != nil && *match.VersionStartIncluding != "" { constraints = append(constraints, fmt.Sprintf(">= %s", *match.VersionStartIncluding)) } else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != "" { constraints = append(constraints, fmt.Sprintf("> %s", *match.VersionStartExcluding)) } if match.VersionEndIncluding != nil && *match.VersionEndIncluding != "" { constraints = append(constraints, fmt.Sprintf("<= %s", *match.VersionEndIncluding)) } else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != "" { constraints = append(constraints, fmt.Sprintf("< %s", *match.VersionEndExcluding)) } if len(constraints) == 0 { c, err := cpe.NewItemFromFormattedString(match.Criteria) if err != nil { return "" } version := c.Version().String() update := c.Update().String() if version != ANY && version != NA { if update != ANY && update != NA { version = fmt.Sprintf("%s-%s", version, update) } constraints = append(constraints, fmt.Sprintf("= %s", version)) } } return strings.Join(constraints, ", ") } func removeDuplicateConstraints(constraints []string) []string { constraintMap := make(map[string]struct{}) var uniqueConstraints []string for _, constraint := range constraints { if _, exists := constraintMap[constraint]; !exists { constraintMap[constraint] = struct{}{} uniqueConstraints = append(uniqueConstraints, constraint) } } return uniqueConstraints } ================================================ FILE: grype/db/v5/build/transformers/nvd/unique_pkg_test.go ================================================ package nvd import ( "testing" "github.com/google/go-cmp/cmp" "github.com/scylladb/go-set/strset" "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" ) func newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker { set := newUniquePkgTracker() for _, c := range candidates { set[c] = nil } return set } func TestFindUniquePkgs(t *testing.T) { tests := []struct { name string config Config nodes []nvd.Node operator *nvd.Operator expected uniquePkgTracker }{ { name: "simple-match", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, }, }, expected: newUniquePkgTrackerFromSlice( []pkgCandidate{ { Product: "product", Vendor: "vendor", TargetSoftware: "target", }, }), }, { name: "skip-hw", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, }, }, expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), }, { name: "skip-os-by-default", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, }, }, expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), }, { name: "include-os-explicitly", config: Config{ CPEParts: strset.New("a", "o"), }, nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, }, }, expected: newUniquePkgTrackerFromSlice([]pkgCandidate{ { Product: "product", Vendor: "vendor", TargetSoftware: "target", }, }), }, { name: "duplicate-by-product", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*", Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, Operator: "OR", }, }, expected: newUniquePkgTrackerFromSlice( []pkgCandidate{ { Product: "productA", Vendor: "vendor", TargetSoftware: "target", }, { Product: "productB", Vendor: "vendor", TargetSoftware: "target", }, }), }, { name: "duplicate-by-target", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*", Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*", Vulnerable: true, }, }, Operator: "OR", }, }, expected: newUniquePkgTrackerFromSlice( []pkgCandidate{ { Product: "product", Vendor: "vendor", TargetSoftware: "targetA", }, { Product: "product", Vendor: "vendor", TargetSoftware: "targetB", }, }), }, { name: "duplicate-by-vendor", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*", Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, Operator: "OR", }, }, expected: newUniquePkgTrackerFromSlice( []pkgCandidate{ { Product: "product", Vendor: "vendorA", TargetSoftware: "target", }, { Product: "product", Vendor: "vendorB", TargetSoftware: "target", }, }), }, { name: "de-duplicate-case", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F", Vulnerable: true, }, { Criteria: "cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V", Vulnerable: true, }, }, Operator: "OR", }, }, expected: newUniquePkgTrackerFromSlice( []pkgCandidate{ { Product: "product", Vendor: "vendor", TargetSoftware: "target", }, }), }, { name: "duplicate-from-nested-nodes", nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, Operator: "OR", }, { CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*", Vulnerable: true, }, }, Operator: "OR", }, }, expected: newUniquePkgTrackerFromSlice( []pkgCandidate{ { Product: "product", Vendor: "vendorA", TargetSoftware: "target", }, { Product: "product", Vendor: "vendorB", TargetSoftware: "target", }, }), }, { name: "cpe with multiple platforms", operator: opRef(nvd.And), nodes: []nvd.Node{ { Negate: boolRef(false), Operator: nvd.Or, CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", MatchCriteriaID: "902B8056-9E37-443B-8905-8AA93E2447FB", Vulnerable: false, }, { Criteria: "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", MatchCriteriaID: "3D94DA3B-FA74-4526-A0A0-A872684598C6", Vulnerable: false, }, { Criteria: "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", MatchCriteriaID: "DEECE5FC-CACF-4496-A3E7-164736409252", Vulnerable: false, }, { Criteria: "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", MatchCriteriaID: "07B237A9-69A3-4A9C-9DA0-4E06BD37AE73", Vulnerable: false, }, { Criteria: "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", MatchCriteriaID: "FA6FEEC2-9F11-4643-8827-749718254FED", Vulnerable: false, }, }, }, { Negate: boolRef(false), Operator: nvd.Or, CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*", MatchCriteriaID: "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6", Vulnerable: true, }, }, }, }, expected: newUniquePkgTrackerFromSlice([]pkgCandidate{ { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", }, }), }, { name: "single platform CPE as first element", operator: opRef(nvd.And), nodes: []nvd.Node{ { Negate: boolRef(false), Operator: nvd.Or, CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", MatchCriteriaID: "902B8056-9E37-443B-8905-8AA93E2447FB", Vulnerable: false, }, }, }, { Negate: boolRef(false), Operator: nvd.Or, CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", VersionEndExcluding: strRef("1.22.2"), VersionStartIncluding: strRef("1.22"), MatchCriteriaID: "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6", Vulnerable: true, }, { Criteria: "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", VersionEndExcluding: strRef("1.21.8"), MatchCriteriaID: "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6", Vulnerable: true, }, }, }, }, expected: newUniquePkgTrackerFromSlice([]pkgCandidate{ { Product: "go", Vendor: "golang", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }, }), }, { name: "single platform CPE as last element", operator: opRef(nvd.And), nodes: []nvd.Node{ { Negate: boolRef(false), Operator: nvd.Or, CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", VersionEndExcluding: strRef("1.22.2"), VersionStartIncluding: strRef("1.22"), MatchCriteriaID: "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6", Vulnerable: true, }, { Criteria: "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", VersionEndExcluding: strRef("1.21.8"), MatchCriteriaID: "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6", Vulnerable: true, }, }, }, { Negate: boolRef(false), Operator: nvd.Or, CpeMatch: []nvd.CpeMatch{ { Criteria: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", MatchCriteriaID: "902B8056-9E37-443B-8905-8AA93E2447FB", Vulnerable: false, }, }, }, }, expected: newUniquePkgTrackerFromSlice([]pkgCandidate{ { Product: "go", Vendor: "golang", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }, }), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.config == (Config{}) { test.config = defaultConfig() } actual := findUniquePkgs(test.config, nvd.Configuration{Nodes: test.nodes, Operator: test.operator}) missing, extra := test.expected.Diff(actual) if len(missing) != 0 { for _, c := range missing { t.Errorf("missing candidate: %+v", c) } } if len(extra) != 0 { for _, c := range extra { t.Errorf("extra candidate: %+v", c) } } }) } } func strRef(s string) *string { return &s } func TestBuildConstraints(t *testing.T) { tests := []struct { name string matches []nvd.CpeMatch expected string }{ { name: "Equals", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", }, }, expected: "= 2.2.0", }, { name: "VersionEndExcluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionEndExcluding: strRef("2.3.0"), }, }, expected: "< 2.3.0", }, { name: "VersionEndIncluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionEndIncluding: strRef("2.3.0"), }, }, expected: "<= 2.3.0", }, { name: "VersionStartExcluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartExcluding: strRef("2.3.0"), }, }, expected: "> 2.3.0", }, { name: "VersionStartIncluding", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), }, }, expected: ">= 2.3.0", }, { name: "Version Range", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndIncluding: strRef("2.5.0"), }, }, expected: ">= 2.3.0, <= 2.5.0", }, { name: "Multiple Version Ranges", matches: []nvd.CpeMatch{ { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartIncluding: strRef("2.3.0"), VersionEndIncluding: strRef("2.5.0"), }, { Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", VersionStartExcluding: strRef("3.3.0"), VersionEndExcluding: strRef("3.5.0"), }, }, expected: ">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := buildConstraints(test.matches) if actual != test.expected { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(actual, test.expected, true) t.Errorf("Expected: %q", test.expected) t.Errorf("Got : %q", actual) t.Errorf("Diff : %q", dmp.DiffPrettyText(diffs)) } }) } } func Test_UniquePackageTrackerHandlesOnlyPlatformDiff(t *testing.T) { candidates := []pkgCandidate{ { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", }, { Product: "redis", Vendor: "redis", TargetSoftware: ANY, PlatformCPE: "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", }, } cpeMatch := nvd.CpeMatch{ Criteria: "cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*", MatchCriteriaID: "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6", } applicationNode := nvd.CpeMatch{ Criteria: "cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*", MatchCriteriaID: "some-uuid", Vulnerable: true, } tracker := newUniquePkgTracker() for _, c := range candidates { candidate, err := newPkgCandidate(defaultConfig(), applicationNode, c.PlatformCPE) require.NoError(t, err) tracker.Add(*candidate, cpeMatch) } assert.Len(t, tracker, len(candidates)) } func TestPlatformPackageCandidates(t *testing.T) { type testCase struct { name string config Config state nvd.Configuration wantChanged bool wantSet uniquePkgTracker } tests := []testCase{ { name: "application X platform", state: nvd.Configuration{ Negate: nil, Nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*", }, { Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*", }, }, Negate: nil, Operator: nvd.Or, }, { CpeMatch: []nvd.CpeMatch{ { Vulnerable: false, Criteria: "cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*", }, { Vulnerable: false, Criteria: "cpe:2.3:o:some-vendor:other-platform:*:*:*:*:*:*:*:*", }, }, Negate: nil, Operator: nvd.Or, }, }, Operator: opRef(nvd.And), }, wantChanged: true, wantSet: newUniquePkgTrackerFromSlice( []pkgCandidate{ mustNewPackage(t, nvd.CpeMatch{ Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*", }, "cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*"), mustNewPackage(t, nvd.CpeMatch{ Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*", }, "cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*"), mustNewPackage(t, nvd.CpeMatch{ Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*", }, "cpe:2.3:o:some-vendor:other-platform:*:*:*:*:*:*:*:*"), mustNewPackage(t, nvd.CpeMatch{ Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*", }, "cpe:2.3:o:some-vendor:other-platform:*:*:*:*:*:*:*:*"), }, ), }, { name: "top-level OR is excluded", state: nvd.Configuration{ Operator: opRef(nvd.Or), }, wantChanged: false, wantSet: newUniquePkgTracker(), }, { name: "top-level nil op is excluded", state: nvd.Configuration{ Operator: nil, }, wantChanged: false, }, { name: "single hardware node results in exclusion", state: nvd.Configuration{ Negate: nil, Nodes: []nvd.Node{ { CpeMatch: []nvd.CpeMatch{ { Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*", }, { Vulnerable: true, Criteria: "cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*", }, }, Negate: nil, Operator: nvd.Or, }, { CpeMatch: []nvd.CpeMatch{ { Vulnerable: false, Criteria: "cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*", }, { Vulnerable: false, Criteria: "cpe:2.3:h:some-vendor:some-device:*:*:*:*:*:*:*:*", }, }, Negate: nil, Operator: nvd.Or, }, }, Operator: opRef(nvd.And), }, wantChanged: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.config == (Config{}) { tc.config = defaultConfig() } set := newUniquePkgTracker() result := platformPackageCandidates(tc.config, set, tc.state) assert.Equal(t, result, tc.wantChanged) if tc.wantSet == nil { tc.wantSet = newUniquePkgTracker() } if diff := cmp.Diff(tc.wantSet.All(), set.All()); diff != "" { t.Errorf("unexpected diff (-want +got)\n%s", diff) } }) } } func opRef(op nvd.Operator) *nvd.Operator { return &op } func boolRef(b bool) *bool { return &b } func mustNewPackage(t *testing.T, match nvd.CpeMatch, platformCPE string, cfg ...Config) pkgCandidate { var tCfg *Config switch len(cfg) { case 0: c := defaultConfig() tCfg = &c case 1: tCfg = &cfg[0] default: t.Fatalf("too many configs provided") } p, err := newPkgCandidate(*tCfg, match, platformCPE) require.NoError(t, err) return *p } ================================================ FILE: grype/db/v5/build/transformers/nvd/unique_pkg_tracker.go ================================================ package nvd import ( "sort" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" ) type uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch func newUniquePkgTracker() uniquePkgTracker { return make(uniquePkgTracker) } func (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) { for k := range s { if !other.Contains(k) { missing = append(missing, k) } } for k := range other { if !s.Contains(k) { extra = append(extra, k) } } return } func (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch { return s[i] } func (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) { if _, ok := s[i]; !ok { s[i] = make([]nvd.CpeMatch, 0) } s[i] = append(s[i], match) } func (s uniquePkgTracker) Remove(i pkgCandidate) { delete(s, i) } func (s uniquePkgTracker) Contains(i pkgCandidate) bool { _, ok := s[i] return ok } func (s uniquePkgTracker) All() []pkgCandidate { res := make([]pkgCandidate, len(s)) idx := 0 for k := range s { res[idx] = k idx++ } sort.SliceStable(res, func(i, j int) bool { return res[i].String() < res[j].String() }) return res } ================================================ FILE: grype/db/v5/build/transformers/os/testdata/alpine-3.9.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "xen", "NamespaceName": "alpine:3.9", "Version": "4.11.1-r0", "VersionFormat": "apk" } ], "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", "Metadata": { "NVD": { "CVSSv2": { "Score": 4.9, "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" } } }, "Name": "CVE-2018-19967", "NamespaceName": "alpine:3.9", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/amazon-multiple-kernel-advisories.json ================================================ [ { "Vulnerability": { "Name": "ALAS-2021-1704", "NamespaceName": "amzn:2", "Description": "", "Severity": "Medium", "Metadata": { "CVE": [ { "Name": "CVE-2021-3653" }, { "Name": "CVE-2021-3656" }, { "Name": "CVE-2021-3732" } ] }, "Link": "https://alas.aws.amazon.com/AL2/ALAS-2021-1704.html", "FixedIn": [ { "Name": "kernel-headers", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "4.14.246-187.474.amzn2" }, { "Name": "kernel", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "4.14.246-187.474.amzn2" } ] } }, { "Vulnerability": { "Name": "ALASKERNEL-5.4-2022-007", "NamespaceName": "amzn:2", "Description": "", "Severity": "Medium", "Metadata": { "CVE": [ { "Name": "CVE-2021-3753" }, { "Name": "CVE-2021-40490" } ] }, "Link": "https://alas.aws.amazon.com/AL2/ALASKERNEL-5.4-2022-007.html", "FixedIn": [ { "Name": "kernel-headers", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.4.144-69.257.amzn2" }, { "Name": "kernel", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.4.144-69.257.amzn2" } ] } }, { "Vulnerability": { "Name": "ALASKERNEL-5.10-2022-005", "NamespaceName": "amzn:2", "Description": "", "Severity": "Medium", "Metadata": { "CVE": [ { "Name": "CVE-2021-3753" }, { "Name": "CVE-2021-40490" } ] }, "Link": "https://alas.aws.amazon.com/AL2/ALASKERNEL-5.10-2022-005.html", "FixedIn": [ { "Name": "kernel-headers", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.10.62-55.141.amzn2" }, { "Name": "kernel", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.10.62-55.141.amzn2" } ] } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/amzn.json ================================================ [ { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "389-ds-base", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-debuginfo", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-devel", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-libs", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-snmp", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" } ], "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", "Metadata": { "CVE": [ { "Name": "CVE-2018-14648" } ] }, "Name": "ALAS-2018-1106", "NamespaceName": "amzn:2", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/azure-linux-3.json ================================================ [ { "Vulnerability": { "Name": "CVE-2023-29403", "NamespaceName": "mariner:3.0", "Description": "CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", "Severity": "High", "Link": "https://nvd.nist.gov/vuln/detail/CVE-2023-29403", "CVSS": [], "FixedIn": [ { "Name": "golang", "NamespaceName": "mariner:3.0", "VersionFormat": "rpm", "Version": "0:1.20.7-1.azl3", "Module": "", "VendorAdvisory": { "NoAdvisory": false, "AdvisorySummary": [] } } ], "Metadata": {} } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/debian-8-multiple-entries-for-same-package.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "rsyslog", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "5.7.4-1", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", "Metadata": { "NVD": { "CVSSv2": { "Score": 2.1, "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" } } }, "Name": "CVE-2011-4623", "NamespaceName": "debian:8", "Severity": "Low" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "rsyslog", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "3.18.6-1", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", "Metadata": { "NVD": { "CVSSv2": { "Score": 5, "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" } } }, "Name": "CVE-2008-5618", "NamespaceName": "debian:8", "Severity": "Low" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/debian-8.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "asterisk", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "1:1.6.2.0~rc3-1", "VersionFormat": "dpkg" }, { "Name": "auth2db", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "0.2.5-2+dfsg-1", "VersionFormat": "dpkg" }, { "Name": "exaile", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "0.2.14+debian-2.2", "VersionFormat": "dpkg" }, { "Name": "wordpress", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", "Metadata": { "NVD": { "CVSSv2": { "Score": 7.5, "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" } } }, "Name": "CVE-2008-7220", "NamespaceName": "debian:8", "Severity": "High" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/mariner-20.json ================================================ [ { "Vulnerability": { "Name": "CVE-2021-37621", "NamespaceName": "mariner:2.0", "Description": "CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.", "Severity": "Medium", "Link": "https://nvd.nist.gov/vuln/detail/CVE-2021-37621", "CVSS": [], "FixedIn": [ { "Name": "exiv2", "NamespaceName": "mariner:2.0", "VersionFormat": "rpm", "Version": "0:0.27.5-1.cm2", "Module": "", "VendorAdvisory": { "NoAdvisory": false, "AdvisorySummary": [] } } ], "Metadata": {} } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/mariner-range.json ================================================ [ { "Vulnerability": { "Name": "CVE-2023-29404", "NamespaceName": "mariner:2.0", "Description": "CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", "Severity": "Critical", "Link": "https://nvd.nist.gov/vuln/detail/CVE-2023-29404", "CVSS": [], "FixedIn": [ { "Name": "golang", "NamespaceName": "mariner:2.0", "VersionFormat": "rpm", "Version": "0:1.20.7-1.cm2", "Module": "", "VendorAdvisory": { "NoAdvisory": false, "AdvisorySummary": [] }, "VulnerableRange": "> 0:1.19.0.cm2, < 0:1.20.7-1.cm2" } ], "Metadata": {} } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/ol-8-modules.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", "FixedIn": [ { "Module": "postgresql:10", "Name": "postgresql", "NamespaceName": "ol:8", "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", "VersionFormat": "rpm" }, { "Module": "postgresql:12", "Name": "postgresql", "NamespaceName": "ol:8", "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", "VersionFormat": "rpm" }, { "Module": "postgresql:9.6", "Name": "postgresql", "NamespaceName": "ol:8", "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", "Metadata": {}, "Name": "CVE-2020-14350", "NamespaceName": "ol:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/ol-8.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "libexif", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-devel", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-dummy", "NamespaceName": "ol:8", "Version": "None", "VersionFormat": "rpm" } ], "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", "Metadata": { "CVE": [ { "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", "Name": "CVE-2020-13112" } ], "Issued": "2020-06-15", "RefId": "ELSA-2020-2550" }, "Name": "ELSA-2020-2550", "NamespaceName": "ol:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/photon-4.0.json ================================================ [ { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "curl", "NamespaceName": "photon:4.0", "Version": "7.88.1-4.ph4", "VersionFormat": "rpm", "VulnerableRange": "< 7.88.1-4.ph4" } ], "Link": "https://nvd.nist.gov/vuln/detail/CVE-2023-38545", "Metadata": {}, "Name": "CVE-2023-38545", "NamespaceName": "photon:4.0", "Severity": "Critical", "CVSS": [] } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/rhel-8-eus.json ================================================ [ { "Vulnerability": { "CVSS": [ { "base_metrics": { "base_score": 8.8, "base_severity": "High", "exploitability_score": 2.8, "impact_score": 5.9 }, "status": "verified", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", "version": "3.1" } ], "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", "FixedIn": [ { "Name": "firefox", "NamespaceName": "rhel:8+eus", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:1341", "Link": "https://access.redhat.com/errata/RHSA-2020:1341" } ], "NoAdvisory": false }, "Version": "0:68.6.1-1.el8_1", "VersionFormat": "rpm" }, { "Name": "thunderbird", "NamespaceName": "rhel:8+eus", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:1495", "Link": "https://access.redhat.com/errata/RHSA-2020:1495" } ], "NoAdvisory": false }, "Version": "0:68.7.0-1.el8_1", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", "Metadata": {}, "Name": "CVE-2020-6819", "NamespaceName": "rhel:8+eus", "Severity": "Critical" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/rhel-8-modules.json ================================================ [ { "Vulnerability": { "CVSS": [ { "base_metrics": { "base_score": 7.1, "base_severity": "High", "exploitability_score": 1.2, "impact_score": 5.9 }, "status": "verified", "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", "version": "3.1" } ], "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", "FixedIn": [ { "Module": "postgresql:10", "Name": "postgresql", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:3669", "Link": "https://access.redhat.com/errata/RHSA-2020:3669" } ], "NoAdvisory": false }, "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", "VersionFormat": "rpm" }, { "Module": "postgresql:12", "Name": "postgresql", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:5620", "Link": "https://access.redhat.com/errata/RHSA-2020:5620" } ], "NoAdvisory": false }, "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", "VersionFormat": "rpm" }, { "Module": "postgresql:9.6", "Name": "postgresql", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:5619", "Link": "https://access.redhat.com/errata/RHSA-2020:5619" } ], "NoAdvisory": false }, "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", "Metadata": {}, "Name": "CVE-2020-14350", "NamespaceName": "rhel:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/rhel-8.json ================================================ [ { "Vulnerability": { "CVSS": [ { "base_metrics": { "base_score": 8.8, "base_severity": "High", "exploitability_score": 2.8, "impact_score": 5.9 }, "status": "verified", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", "version": "3.1" } ], "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", "FixedIn": [ { "Name": "firefox", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:1341", "Link": "https://access.redhat.com/errata/RHSA-2020:1341" } ], "NoAdvisory": false }, "Version": "0:68.6.1-1.el8_1", "VersionFormat": "rpm" }, { "Name": "thunderbird", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:1495", "Link": "https://access.redhat.com/errata/RHSA-2020:1495" } ], "NoAdvisory": false }, "Version": "0:68.7.0-1.el8_1", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", "Metadata": {}, "Name": "CVE-2020-6819", "NamespaceName": "rhel:8", "Severity": "Critical" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/testdata/unmarshal-test.json ================================================ [ { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "389-ds-base", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-debuginfo", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-devel", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-libs", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-snmp", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" } ], "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", "Metadata": { "CVE": [ {"Name": "CVE-2018-14648"} ] }, "Name": "ALAS-2018-1106", "NamespaceName": "amzn:2", "Severity": "Medium" } }, { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "kernel-livepatch-4.14.173-137.228", "NamespaceName": "amzn:2", "Version": "1.0-3.amzn2", "VersionFormat": "rpm" }, { "Name": "kernel-livepatch-4.14.173-137.228-debuginfo", "NamespaceName": "amzn:2", "Version": "1.0-3.amzn2", "VersionFormat": "rpm" } ], "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html", "Metadata": { "CVE": [ {"Name": "CVE-2020-12657"} ] }, "Name": "ALASLIVEPATCH-2020-012", "NamespaceName": "amzn:2", "Severity": "High" } }, { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "kernel-livepatch-4.14.171-136.231", "NamespaceName": "amzn:2", "Version": "1.0-5.amzn2", "VersionFormat": "rpm" }, { "Name": "kernel-livepatch-4.14.171-136.231-debuginfo", "NamespaceName": "amzn:2", "Version": "1.0-5.amzn2", "VersionFormat": "rpm" } ], "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html", "Metadata": { "CVE": [ {"Name": "CVE-2020-12657"} ] }, "Name": "ALASLIVEPATCH-2020-011", "NamespaceName": "amzn:2", "Severity": "High" } } ] ================================================ FILE: grype/db/v5/build/transformers/os/transform.go ================================================ package os // nolint:revive import ( "fmt" "strings" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/build/transformers" "github.com/anchore/grype/grype/db/v5/namespace" "github.com/anchore/grype/grype/db/v5/pkg/qualifier" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" "github.com/anchore/grype/grype/distro" ) func buildGrypeNamespace(group string) (namespace.Namespace, error) { feedGroupComponents := strings.Split(group, ":") if len(feedGroupComponents) < 2 { return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) } // Currently known enterprise feed groups are expected to be of the form {distroID}:{version} feedGroupDistroID := feedGroupComponents[0] // secureos and photon are not supported in the grype v5 schema, so the records should be dropped entirely if feedGroupDistroID == "secureos" || feedGroupDistroID == "photon" { return nil, nil } d, ok := distro.IDMapping[feedGroupDistroID] if !ok { return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) } providerName := d.String() distroName := d.String() ver := feedGroupComponents[1] switch d { case distro.OracleLinux: providerName = "oracle" case distro.AmazonLinux: providerName = "amazon" case distro.Mariner, distro.Azure: providerName = "mariner" if strings.HasPrefix(ver, "3") { distroName = distro.Azure.String() // Mariner Linux 3 is known as "Azure Linux 3" } } // distro channels are not supported in the grype v5 schema, so the records should be dropped entirely if strings.Contains(ver, "+") { return nil, nil } ns, err := namespace.FromString(fmt.Sprintf("%s:distro:%s:%s", providerName, distroName, ver)) if err != nil { return nil, err } return ns, nil } func buildPackageQualifiers(fixedInEntry unmarshal.OSFixedIn) (qualifiers []qualifier.Qualifier) { if fixedInEntry.VersionFormat == "rpm" { module := "" if fixedInEntry.Module != nil { module = *fixedInEntry.Module } qualifiers = []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: module, }} } return qualifiers } func Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { var allVulns []db.Vulnerability // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) recordSource := fmt.Sprintf("vulnerabilities:%s", vulnerability.Vulnerability.NamespaceName) grypeNamespace, err := buildGrypeNamespace(vulnerability.Vulnerability.NamespaceName) if err != nil { return nil, err } if grypeNamespace == nil { // this is an enterprise feed group that does not have a corresponding grype namespace, so skip it return nil, nil } entryNamespace := grypeNamespace.String() // there may be multiple packages indicated within the FixedIn field, we should make // separate vulnerability entries (one for each name|namespace combo) while merging // constraint ranges as they are found. for idx, fixedInEntry := range vulnerability.Vulnerability.FixedIn { // create vulnerability entry allVulns = append(allVulns, db.Vulnerability{ ID: vulnerability.Vulnerability.Name, PackageQualifiers: buildPackageQualifiers(fixedInEntry), VersionConstraint: enforceConstraint(fixedInEntry.Version, fixedInEntry.VulnerableRange, fixedInEntry.VersionFormat, vulnerability.Vulnerability.Name), VersionFormat: fixedInEntry.VersionFormat, PackageName: grypeNamespace.Resolver().Normalize(fixedInEntry.Name), Namespace: entryNamespace, RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), Fix: getFix(vulnerability, idx), Advisories: getAdvisories(vulnerability, idx), }) } // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) metadata := db.VulnerabilityMetadata{ ID: vulnerability.Vulnerability.Name, Namespace: entryNamespace, DataSource: vulnerability.Vulnerability.Link, RecordSource: recordSource, Severity: vulnerability.Vulnerability.Severity, URLs: getLinks(vulnerability), Description: vulnerability.Vulnerability.Description, Cvss: getCvss(vulnerability), } return transformers.NewEntries(allVulns, metadata), nil } func getLinks(entry unmarshal.OSVulnerability) []string { // find all URLs related to the vulnerability links := []string{entry.Vulnerability.Link} if entry.Vulnerability.Metadata.CVE != nil { for _, cve := range entry.Vulnerability.Metadata.CVE { if cve.Link != "" { links = append(links, cve.Link) } } } return links } func getCvss(entry unmarshal.OSVulnerability) (cvss []db.Cvss) { for _, vendorCvss := range entry.Vulnerability.CVSS { cvss = append(cvss, db.Cvss{ Version: vendorCvss.Version, Vector: vendorCvss.VectorString, Metrics: db.NewCvssMetrics( vendorCvss.BaseMetrics.BaseScore, vendorCvss.BaseMetrics.ExploitabilityScore, vendorCvss.BaseMetrics.ImpactScore, ), VendorMetadata: transformers.VendorBaseMetrics{ BaseSeverity: vendorCvss.BaseMetrics.BaseSeverity, Status: vendorCvss.Status, }, }) } return cvss } func getAdvisories(entry unmarshal.OSVulnerability, idx int) (advisories []db.Advisory) { fixedInEntry := entry.Vulnerability.FixedIn[idx] for _, advisory := range fixedInEntry.VendorAdvisory.AdvisorySummary { advisories = append(advisories, db.Advisory{ ID: advisory.ID, Link: advisory.Link, }) } return advisories } func getFix(entry unmarshal.OSVulnerability, idx int) db.Fix { fixedInEntry := entry.Vulnerability.FixedIn[idx] var fixedInVersions []string fixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Version) if fixedInVersion != "" { fixedInVersions = append(fixedInVersions, fixedInVersion) } fixState := db.NotFixedState if len(fixedInVersions) > 0 { fixState = db.FixedState } else if fixedInEntry.VendorAdvisory.NoAdvisory { fixState = db.WontFixState } return db.Fix{ Versions: fixedInVersions, State: fixState, } } func getRelatedVulnerabilities(entry unmarshal.OSVulnerability) (vulns []db.VulnerabilityReference) { // associate related vulnerabilities from the NVD namespace if strings.HasPrefix(entry.Vulnerability.Name, "CVE") { vulns = append(vulns, db.VulnerabilityReference{ ID: entry.Vulnerability.Name, Namespace: "nvd:cpe", }) } // note: an example of multiple CVEs for a record is centos:5 RHSA-2007:0055 which maps to CVE-2007-0002 and CVE-2007-1466 for _, ref := range entry.Vulnerability.Metadata.CVE { vulns = append(vulns, db.VulnerabilityReference{ ID: ref.Name, Namespace: "nvd:cpe", }) } return vulns } func deriveConstraintFromFix(fixVersion, vulnerabilityID string) string { constraint := fmt.Sprintf("< %s", fixVersion) if strings.HasPrefix(vulnerabilityID, "ALASKERNEL-") { // Amazon advisories of the form ALASKERNEL-5.4-2023-048 should be interpreted as only applying to // the 5.4.x kernel line since Amazon issue a separate advisory per affected line, thus the constraint // should be >= 5.4, < {fix version}. In the future the vunnel schema for OS vulns should be enhanced // to emit actual constraints rather than fixed-in entries (tracked in https://github.com/anchore/vunnel/issues/266) // at which point this workaround in grype-db can be removed. components := strings.Split(vulnerabilityID, "-") if len(components) == 4 { base := components[1] constraint = fmt.Sprintf(">= %s, < %s", base, fixVersion) } } return constraint } func enforceConstraint(fixedVersion, vulnerableRange, format, vulnerabilityID string) string { if len(vulnerableRange) > 0 { return vulnerableRange } fixedVersion = versionutil.CleanConstraint(fixedVersion) if len(fixedVersion) == 0 { return "" } switch strings.ToLower(format) { case "semver": return versionutil.EnforceSemVerConstraint(fixedVersion) default: // the passed constraint is a fixed version return deriveConstraintFromFix(fixedVersion, vulnerabilityID) } } ================================================ FILE: grype/db/v5/build/transformers/os/transform_test.go ================================================ package os import ( "os" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/build/transformers" "github.com/anchore/grype/grype/db/v5/pkg/qualifier" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" ) func TestUnmarshalOSVulnerabilitiesEntries(t *testing.T) { f, err := os.Open("testdata/unmarshal-test.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.OSVulnerabilityEntries(f) require.NoError(t, err) assert.Len(t, entries, 3) } func TestParseVulnerabilitiesEntry(t *testing.T) { tests := []struct { name string numEntries int fixture string vulns []db.Vulnerability metadata db.VulnerabilityMetadata }{ { name: "Amazon", numEntries: 1, fixture: "testdata/amzn.json", vulns: []db.Vulnerability{ { ID: "ALAS-2018-1106", VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2018-14648", Namespace: "nvd:cpe", }, }, PackageName: "389-ds-base", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, Namespace: "amazon:distro:amazonlinux:2", Fix: db.Fix{ Versions: []string{"1.3.8.4-15.amzn2.0.1"}, State: db.FixedState, }, }, { ID: "ALAS-2018-1106", VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2018-14648", Namespace: "nvd:cpe", }, }, PackageName: "389-ds-base-debuginfo", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, Namespace: "amazon:distro:amazonlinux:2", Fix: db.Fix{ Versions: []string{"1.3.8.4-15.amzn2.0.1"}, State: db.FixedState, }, }, { ID: "ALAS-2018-1106", VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2018-14648", Namespace: "nvd:cpe", }, }, PackageName: "389-ds-base-devel", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, Namespace: "amazon:distro:amazonlinux:2", Fix: db.Fix{ Versions: []string{"1.3.8.4-15.amzn2.0.1"}, State: db.FixedState, }, }, { ID: "ALAS-2018-1106", VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2018-14648", Namespace: "nvd:cpe", }, }, PackageName: "389-ds-base-libs", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, Namespace: "amazon:distro:amazonlinux:2", Fix: db.Fix{ Versions: []string{"1.3.8.4-15.amzn2.0.1"}, State: db.FixedState, }, }, { ID: "ALAS-2018-1106", VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2018-14648", Namespace: "nvd:cpe", }, }, PackageName: "389-ds-base-snmp", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, Namespace: "amazon:distro:amazonlinux:2", Fix: db.Fix{ Versions: []string{"1.3.8.4-15.amzn2.0.1"}, State: db.FixedState, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "ALAS-2018-1106", Namespace: "amazon:distro:amazonlinux:2", DataSource: "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", RecordSource: "vulnerabilities:amzn:2", Severity: "Medium", URLs: []string{"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html"}, }, }, { name: "Debian", numEntries: 1, fixture: "testdata/debian-8.json", vulns: []db.Vulnerability{ { ID: "CVE-2008-7220", PackageName: "asterisk", VersionConstraint: "< 1:1.6.2.0~rc3-1", VersionFormat: "dpkg", Namespace: "debian:distro:debian:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2008-7220", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"1:1.6.2.0~rc3-1"}, State: db.FixedState, }, }, { ID: "CVE-2008-7220", PackageName: "auth2db", VersionConstraint: "< 0.2.5-2+dfsg-1", VersionFormat: "dpkg", Namespace: "debian:distro:debian:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2008-7220", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0.2.5-2+dfsg-1"}, State: db.FixedState, }, }, { ID: "CVE-2008-7220", PackageName: "exaile", VersionConstraint: "< 0.2.14+debian-2.2", VersionFormat: "dpkg", Namespace: "debian:distro:debian:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2008-7220", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0.2.14+debian-2.2"}, State: db.FixedState, }, }, { ID: "CVE-2008-7220", PackageName: "wordpress", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2008-7220", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ State: db.NotFixedState, }, VersionConstraint: "", VersionFormat: "dpkg", Namespace: "debian:distro:debian:8", }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2008-7220", Namespace: "debian:distro:debian:8", DataSource: "https://security-tracker.debian.org/tracker/CVE-2008-7220", RecordSource: "vulnerabilities:debian:8", Severity: "High", URLs: []string{"https://security-tracker.debian.org/tracker/CVE-2008-7220"}, Description: "", }, }, { name: "RHEL", numEntries: 1, fixture: "testdata/rhel-8.json", vulns: []db.Vulnerability{ { ID: "CVE-2020-6819", PackageName: "firefox", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "< 0:68.6.1-1.el8_1", VersionFormat: "rpm", Namespace: "redhat:distro:redhat:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-6819", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:68.6.1-1.el8_1"}, State: db.FixedState, }, Advisories: []db.Advisory{ { ID: "RHSA-2020:1341", Link: "https://access.redhat.com/errata/RHSA-2020:1341", }, }, }, { ID: "CVE-2020-6819", PackageName: "thunderbird", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "< 0:68.7.0-1.el8_1", VersionFormat: "rpm", Namespace: "redhat:distro:redhat:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-6819", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:68.7.0-1.el8_1"}, State: db.FixedState, }, Advisories: []db.Advisory{ { ID: "RHSA-2020:1495", Link: "https://access.redhat.com/errata/RHSA-2020:1495", }, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2020-6819", DataSource: "https://access.redhat.com/security/cve/CVE-2020-6819", Namespace: "redhat:distro:redhat:8", RecordSource: "vulnerabilities:rhel:8", Severity: "Critical", URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-6819"}, Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", Cvss: []db.Cvss{ { Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", Metrics: db.NewCvssMetrics( 8.8, 2.8, 5.9, ), VendorMetadata: transformers.VendorBaseMetrics{ Status: "verified", BaseSeverity: "High", }, }, }, }, }, { name: "RHEL with modularity", numEntries: 1, fixture: "testdata/rhel-8-modules.json", vulns: []db.Vulnerability{ { ID: "CVE-2020-14350", PackageName: "postgresql", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "postgresql:10", }}, VersionConstraint: "< 0:10.14-1.module+el8.2.0+7801+be0fed80", VersionFormat: "rpm", Namespace: "redhat:distro:redhat:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14350", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:10.14-1.module+el8.2.0+7801+be0fed80"}, State: db.FixedState, }, Advisories: []db.Advisory{ { ID: "RHSA-2020:3669", Link: "https://access.redhat.com/errata/RHSA-2020:3669", }, }, }, { ID: "CVE-2020-14350", PackageName: "postgresql", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "postgresql:12", }}, VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", VersionFormat: "rpm", Namespace: "redhat:distro:redhat:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14350", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, State: db.FixedState, }, Advisories: []db.Advisory{ { ID: "RHSA-2020:5620", Link: "https://access.redhat.com/errata/RHSA-2020:5620", }, }, }, { ID: "CVE-2020-14350", PackageName: "postgresql", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "postgresql:9.6", }}, VersionConstraint: "< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", VersionFormat: "rpm", Namespace: "redhat:distro:redhat:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14350", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6"}, State: db.FixedState, }, Advisories: []db.Advisory{ { ID: "RHSA-2020:5619", Link: "https://access.redhat.com/errata/RHSA-2020:5619", }, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2020-14350", DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", Namespace: "redhat:distro:redhat:8", RecordSource: "vulnerabilities:rhel:8", Severity: "Medium", URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", Cvss: []db.Cvss{ { Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", Metrics: db.NewCvssMetrics( 7.1, 1.2, 5.9, ), VendorMetadata: transformers.VendorBaseMetrics{ Status: "verified", BaseSeverity: "High", }, }, }, }, }, { name: "RHEL EUS (ignore)", numEntries: 1, fixture: "testdata/rhel-8-eus.json", // intentionally creates no vulnerabilities to write to the DB }, { name: "Photon (ignore)", numEntries: 1, fixture: "testdata/photon-4.0.json", // photon is not supported in v5, records should be dropped entirely }, { name: "Alpine", numEntries: 1, fixture: "testdata/alpine-3.9.json", vulns: []db.Vulnerability{ { ID: "CVE-2018-19967", PackageName: "xen", VersionConstraint: "< 4.11.1-r0", VersionFormat: "apk", Namespace: "alpine:distro:alpine:3.9", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2018-19967", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"4.11.1-r0"}, State: db.FixedState, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2018-19967", DataSource: "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", Namespace: "alpine:distro:alpine:3.9", RecordSource: "vulnerabilities:alpine:3.9", Severity: "Medium", URLs: []string{"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967"}, Description: "", }, }, { name: "Oracle", numEntries: 1, fixture: "testdata/ol-8.json", vulns: []db.Vulnerability{ { ID: "ELSA-2020-2550", PackageName: "libexif", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "< 0:0.6.21-17.el8_2", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-13112", Namespace: "nvd:cpe", }, }, Namespace: "oracle:distro:oraclelinux:8", Fix: db.Fix{ Versions: []string{"0:0.6.21-17.el8_2"}, State: db.FixedState, }, }, { ID: "ELSA-2020-2550", PackageName: "libexif-devel", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "< 0:0.6.21-17.el8_2", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-13112", Namespace: "nvd:cpe", }, }, Namespace: "oracle:distro:oraclelinux:8", Fix: db.Fix{ Versions: []string{"0:0.6.21-17.el8_2"}, State: db.FixedState, }, }, { ID: "ELSA-2020-2550", PackageName: "libexif-dummy", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-13112", Namespace: "nvd:cpe", }, }, Namespace: "oracle:distro:oraclelinux:8", Fix: db.Fix{ Versions: nil, State: db.NotFixedState, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "ELSA-2020-2550", DataSource: "http://linux.oracle.com/errata/ELSA-2020-2550.html", Namespace: "oracle:distro:oraclelinux:8", RecordSource: "vulnerabilities:ol:8", Severity: "Medium", URLs: []string{"http://linux.oracle.com/errata/ELSA-2020-2550.html", "http://linux.oracle.com/cve/CVE-2020-13112.html"}, }, }, { name: "Oracle Linux 8 with modularity", numEntries: 1, fixture: "testdata/ol-8-modules.json", vulns: []db.Vulnerability{ { ID: "CVE-2020-14350", PackageName: "postgresql", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "postgresql:10", }}, VersionConstraint: "< 0:10.14-1.module+el8.2.0+7801+be0fed80", VersionFormat: "rpm", Namespace: "oracle:distro:oraclelinux:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14350", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:10.14-1.module+el8.2.0+7801+be0fed80"}, State: db.FixedState, }, }, { ID: "CVE-2020-14350", PackageName: "postgresql", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "postgresql:12", }}, VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", VersionFormat: "rpm", Namespace: "oracle:distro:oraclelinux:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14350", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, State: db.FixedState, }, }, { ID: "CVE-2020-14350", PackageName: "postgresql", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "postgresql:9.6", }}, VersionConstraint: "< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", VersionFormat: "rpm", Namespace: "oracle:distro:oraclelinux:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2020-14350", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6"}, State: db.FixedState, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2020-14350", DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", Namespace: "oracle:distro:oraclelinux:8", RecordSource: "vulnerabilities:ol:8", Severity: "Medium", URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", }, }, { name: "mariner linux 2.0", numEntries: 1, fixture: "testdata/mariner-20.json", vulns: []db.Vulnerability{ { ID: "CVE-2021-37621", PackageName: "exiv2", Namespace: "mariner:distro:mariner:2.0", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", }, }, RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-37621", Namespace: "nvd:cpe", }, }, VersionConstraint: "< 0:0.27.5-1.cm2", VersionFormat: "rpm", Fix: db.Fix{ Versions: []string{"0:0.27.5-1.cm2"}, State: db.FixedState, }, Advisories: nil, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2021-37621", Namespace: "mariner:distro:mariner:2.0", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2021-37621", RecordSource: "vulnerabilities:mariner:2.0", Severity: "Medium", URLs: []string{"https://nvd.nist.gov/vuln/detail/CVE-2021-37621"}, Description: "CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.", Cvss: nil, }, }, { name: "azure linux 3", numEntries: 1, fixture: "testdata/azure-linux-3.json", vulns: []db.Vulnerability{ { ID: "CVE-2023-29403", PackageName: "golang", Namespace: "mariner:distro:azurelinux:3.0", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", }, }, RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2023-29403", Namespace: "nvd:cpe", }, }, VersionConstraint: "< 0:1.20.7-1.azl3", VersionFormat: "rpm", Fix: db.Fix{ Versions: []string{"0:1.20.7-1.azl3"}, State: db.FixedState, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2023-29403", Namespace: "mariner:distro:azurelinux:3.0", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2023-29403", RecordSource: "vulnerabilities:mariner:3.0", Severity: "High", URLs: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-29403"}, Description: "CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", }, }, { name: "mariner entry with version range", numEntries: 1, fixture: "testdata/mariner-range.json", vulns: []db.Vulnerability{ { ID: "CVE-2023-29404", PackageName: "golang", Namespace: "mariner:distro:mariner:2.0", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }, }, VersionConstraint: "> 0:1.19.0.cm2, < 0:1.20.7-1.cm2", VersionFormat: "rpm", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2023-29404", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"0:1.20.7-1.cm2"}, State: db.FixedState, }, }, }, metadata: db.VulnerabilityMetadata{ ID: "CVE-2023-29404", Namespace: "mariner:distro:mariner:2.0", DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2023-29404", RecordSource: "vulnerabilities:mariner:2.0", Severity: "Critical", URLs: []string{"https://nvd.nist.gov/vuln/detail/CVE-2023-29404"}, Description: "CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { f, err := os.Open(test.fixture) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, f.Close()) }) entries, err := unmarshal.OSVulnerabilityEntries(f) assert.NoError(t, err) assert.Len(t, entries, 1) entry := entries[0] dataEntries, err := Transform(entry) assert.NoError(t, err) var vulns []db.Vulnerability for _, entry := range dataEntries { switch vuln := entry.Data.(type) { case db.Vulnerability: vulns = append(vulns, vuln) case db.VulnerabilityMetadata: assert.Equal(t, test.metadata, vuln) default: t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata: %+v", vuln) } } if diff := cmp.Diff(test.vulns, vulns); diff != "" { t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) } }) } } func TestParseVulnerabilitiesAllEntries(t *testing.T) { tests := []struct { name string numEntries int fixture string vulns []db.Vulnerability }{ { name: "Debian", numEntries: 2, fixture: "testdata/debian-8-multiple-entries-for-same-package.json", vulns: []db.Vulnerability{ { ID: "CVE-2011-4623", PackageName: "rsyslog", VersionConstraint: "< 5.7.4-1", VersionFormat: "dpkg", Namespace: "debian:distro:debian:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2011-4623", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"5.7.4-1"}, State: db.FixedState, }, }, { ID: "CVE-2008-5618", PackageName: "rsyslog", VersionConstraint: "< 3.18.6-1", VersionFormat: "dpkg", Namespace: "debian:distro:debian:8", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2008-5618", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"3.18.6-1"}, State: db.FixedState, }, }, }, }, { name: "Amazon", numEntries: 3, fixture: "testdata/amazon-multiple-kernel-advisories.json", vulns: []db.Vulnerability{ { ID: "ALAS-2021-1704", PackageName: "kernel-headers", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "< 4.14.246-187.474.amzn2", VersionFormat: "rpm", Namespace: "amazon:distro:amazonlinux:2", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-3653", Namespace: "nvd:cpe", }, { ID: "CVE-2021-3656", Namespace: "nvd:cpe", }, { ID: "CVE-2021-3732", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"4.14.246-187.474.amzn2"}, State: db.FixedState, }, }, { ID: "ALAS-2021-1704", PackageName: "kernel", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: "< 4.14.246-187.474.amzn2", VersionFormat: "rpm", Namespace: "amazon:distro:amazonlinux:2", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-3653", Namespace: "nvd:cpe", }, { ID: "CVE-2021-3656", Namespace: "nvd:cpe", }, { ID: "CVE-2021-3732", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"4.14.246-187.474.amzn2"}, State: db.FixedState, }, }, { ID: "ALASKERNEL-5.4-2022-007", PackageName: "kernel-headers", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: ">= 5.4, < 5.4.144-69.257.amzn2", VersionFormat: "rpm", Namespace: "amazon:distro:amazonlinux:2", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-3753", Namespace: "nvd:cpe", }, { ID: "CVE-2021-40490", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"5.4.144-69.257.amzn2"}, State: db.FixedState, }, }, { ID: "ALASKERNEL-5.4-2022-007", PackageName: "kernel", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: ">= 5.4, < 5.4.144-69.257.amzn2", VersionFormat: "rpm", Namespace: "amazon:distro:amazonlinux:2", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-3753", Namespace: "nvd:cpe", }, { ID: "CVE-2021-40490", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"5.4.144-69.257.amzn2"}, State: db.FixedState, }, }, { ID: "ALASKERNEL-5.10-2022-005", PackageName: "kernel-headers", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: ">= 5.10, < 5.10.62-55.141.amzn2", VersionFormat: "rpm", Namespace: "amazon:distro:amazonlinux:2", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-3753", Namespace: "nvd:cpe", }, { ID: "CVE-2021-40490", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"5.10.62-55.141.amzn2"}, State: db.FixedState, }, }, { ID: "ALASKERNEL-5.10-2022-005", PackageName: "kernel", PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }}, VersionConstraint: ">= 5.10, < 5.10.62-55.141.amzn2", VersionFormat: "rpm", Namespace: "amazon:distro:amazonlinux:2", RelatedVulnerabilities: []db.VulnerabilityReference{ { ID: "CVE-2021-3753", Namespace: "nvd:cpe", }, { ID: "CVE-2021-40490", Namespace: "nvd:cpe", }, }, Fix: db.Fix{ Versions: []string{"5.10.62-55.141.amzn2"}, State: db.FixedState, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { f, err := os.Open(test.fixture) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, f.Close()) }) entries, err := unmarshal.OSVulnerabilityEntries(f) assert.NoError(t, err) assert.Len(t, entries, test.numEntries) var vulns []db.Vulnerability for _, entry := range entries { dataEntries, err := Transform(entry) assert.NoError(t, err) for _, entry := range dataEntries { switch vuln := entry.Data.(type) { case db.Vulnerability: vulns = append(vulns, vuln) case db.VulnerabilityMetadata: default: t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata: %+v", vuln) } } } if diff := cmp.Diff(test.vulns, vulns); diff != "" { t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) } }) } } ================================================ FILE: grype/db/v5/build/transformers/vulnerability_metadata.go ================================================ package transformers // VendorBaseMetrics captures extra metrics that do not fit into a common CVSS // struct, like Status and BaseSeverity type VendorBaseMetrics struct { BaseSeverity string `json:"base_severity"` Status string `json:"status"` } ================================================ FILE: grype/db/v5/build/writer.go ================================================ package v5 import ( "crypto/sha256" "encoding/json" "fmt" "os" "path" "path/filepath" "strings" "sync" "time" "github.com/spf13/afero" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/distribution" "github.com/anchore/grype/grype/db/v5/store" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" ) // TODO: add NVDNamespace const to grype.db package? const ( nvdNamespace = "nvd:cpe" providerMetadataFileName = "provider-metadata.json" ) var _ data.Writer = (*writer)(nil) type writer struct { dbPath string store db.Store states provider.States // Batching infrastructure batchSize int batchBuffer []func() error mu sync.Mutex // Protect batch state // Metrics totalBatches int } type ProviderMetadata struct { Providers []Provider `json:"providers"` } type Provider struct { Name string `json:"name"` LastSuccessfulRun time.Time `json:"lastSuccessfulRun"` } func NewWriter(directory string, dataAge time.Time, states provider.States, batchSize int) (data.Writer, error) { dbPath := path.Join(directory, db.VulnerabilityStoreFileName) theStore, err := store.New(dbPath, true) if err != nil { return nil, fmt.Errorf("unable to create store: %w", err) } if err := theStore.SetID(db.NewID(dataAge)); err != nil { return nil, fmt.Errorf("unable to set DB ID: %w", err) } // Use default if not configured if batchSize == 0 { batchSize = 2000 } return &writer{ dbPath: dbPath, store: theStore, states: states, batchSize: batchSize, batchBuffer: make([]func() error, 0, batchSize), }, nil } func (w *writer) Write(entries ...data.Entry) error { log.WithFields("records", len(entries)).Trace("writing records to DB") for _, entry := range entries { if entry.DBSchemaVersion != db.SchemaVersion { return fmt.Errorf("wrong schema version: want %+v got %+v", db.SchemaVersion, entry.DBSchemaVersion) } switch row := entry.Data.(type) { case db.Vulnerability: // Batch the vulnerability write vuln := row if err := w.addToBatch(func() error { return w.store.AddVulnerability(vuln) }); err != nil { return fmt.Errorf("unable to batch vulnerability write: %w", err) } case db.VulnerabilityMetadata: // Normalize severity before batching normalizeSeverity(&row, w.store) metadata := row if err := w.addToBatch(func() error { return w.store.AddVulnerabilityMetadata(metadata) }); err != nil { return fmt.Errorf("unable to batch vulnerability metadata write: %w", err) } case db.VulnerabilityMatchExclusion: // Batch the exclusion write exclusion := row if err := w.addToBatch(func() error { return w.store.AddVulnerabilityMatchExclusion(exclusion) }); err != nil { return fmt.Errorf("unable to batch vulnerability match exclusion write: %w", err) } default: return fmt.Errorf("data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T", row) } } return nil } // addToBatch adds an operation to the batch buffer and flushes if batch size is reached func (w *writer) addToBatch(op func() error) error { w.mu.Lock() defer w.mu.Unlock() w.batchBuffer = append(w.batchBuffer, op) if len(w.batchBuffer) >= w.batchSize { return w.flushUnlocked() } return nil } // Flush executes all pending operations in the batch buffer func (w *writer) Flush() error { w.mu.Lock() defer w.mu.Unlock() return w.flushUnlocked() } // flushUnlocked executes all pending operations without acquiring the lock (must be called with lock held) func (w *writer) flushUnlocked() error { if len(w.batchBuffer) == 0 { return nil } log.WithFields( "operations", len(w.batchBuffer), "batch_size", w.batchSize, ).Debug("flushing batch") for i, op := range w.batchBuffer { if err := op(); err != nil { return fmt.Errorf("batch operation %d failed: %w", i, err) } } w.batchBuffer = w.batchBuffer[:0] w.totalBatches++ return nil } // metadataAndClose closes the database and returns its metadata. // The reason this is a compound action is that getting the built time and // schema version from the database is an operation on the open database, // but the checksum must be computed after the database is compacted and closed. func (w *writer) metadataAndClose() (*distribution.Metadata, error) { storeID, err := w.store.GetID() if err != nil { return nil, fmt.Errorf("failed to fetch store ID: %w", err) } w.store.Close() hashStr, err := file.HashFile(afero.NewOsFs(), w.dbPath, sha256.New()) if err != nil { return nil, fmt.Errorf("failed to hash database file (%s): %w", w.dbPath, err) } metadata := distribution.Metadata{ Built: storeID.BuildTimestamp, Version: storeID.SchemaVersion, Checksum: "sha256:" + hashStr, } return &metadata, nil } func NewProviderMetadata() ProviderMetadata { return ProviderMetadata{ Providers: make([]Provider, 0), } } func (w *writer) ProviderMetadata() *ProviderMetadata { metadata := NewProviderMetadata() // Set provider time from states for _, state := range w.states { metadata.Providers = append(metadata.Providers, Provider{ Name: state.Provider, LastSuccessfulRun: state.Timestamp, }) } return &metadata } func (w *writer) Close() error { // Flush any remaining batched operations if err := w.Flush(); err != nil { return fmt.Errorf("unable to flush pending writes: %w", err) } metadata, err := w.metadataAndClose() if err != nil { return err } metadataPath := path.Join(filepath.Dir(w.dbPath), distribution.MetadataFileName) if err = metadata.Write(metadataPath); err != nil { return err } providerMetadataPath := path.Join(filepath.Dir(w.dbPath), providerMetadataFileName) if err = w.ProviderMetadata().Write(providerMetadataPath); err != nil { return err } log.WithFields( "path", w.dbPath, "total_batches", w.totalBatches, ).Info("database created") log.WithFields("path", metadataPath).Debug("database metadata created") log.WithFields("path", providerMetadataPath).Debug("provider metadata created") return nil } func normalizeSeverity(metadata *db.VulnerabilityMetadata, reader db.VulnerabilityMetadataStoreReader) { metadata.Severity = string(data.ParseSeverity(metadata.Severity)) if metadata.Severity != "" && strings.ToLower(metadata.Severity) != "unknown" { return } if !strings.HasPrefix(strings.ToLower(metadata.ID), "cve") { return } if strings.HasPrefix(metadata.Namespace, nvdNamespace) { return } m, err := reader.GetVulnerabilityMetadata(metadata.ID, nvdNamespace) if err != nil { log.WithFields("id", metadata.ID, "error", err).Warn("error fetching vulnerability metadata from NVD namespace") return } if m == nil { log.WithFields("id", metadata.ID).Trace("unable to find vulnerability metadata from NVD namespace") return } newSeverity := string(data.ParseSeverity(m.Severity)) if newSeverity != metadata.Severity { log.WithFields("id", metadata.ID, "namespace", metadata.Namespace, "sev-from", metadata.Severity, "sev-to", newSeverity).Trace("overriding irrelevant severity with data from NVD record") } metadata.Severity = newSeverity } func (p ProviderMetadata) Write(path string) error { providerMetadataJSON, err := json.MarshalIndent(p, "", " ") if err != nil { return fmt.Errorf("unable to marshal provider metadata: %w", err) } //nolint:gosec if err = os.WriteFile(path, providerMetadataJSON, 0644); err != nil { return fmt.Errorf("unable to write provider metadata: %w", err) } return nil } ================================================ FILE: grype/db/v5/build/writer_test.go ================================================ package v5 import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/db/data" db "github.com/anchore/grype/grype/db/v5" ) var _ db.VulnerabilityMetadataStoreReader = (*mockReader)(nil) type mockReader struct { metadata *db.VulnerabilityMetadata err error } func newMockReader(sev string) *mockReader { return &mockReader{ metadata: &db.VulnerabilityMetadata{ Severity: sev, Namespace: "nvd", }, } } func newDeadMockReader() *mockReader { return &mockReader{ err: errors.New("dead"), } } func (m mockReader) GetVulnerabilityMetadata(_, _ string) (*db.VulnerabilityMetadata, error) { return m.metadata, m.err } func (m mockReader) GetAllVulnerabilityMetadata() (*[]db.VulnerabilityMetadata, error) { panic("implement me") } func Test_normalizeSeverity(t *testing.T) { tests := []struct { name string initialSeverity string namespace string cveID string reader db.VulnerabilityMetadataStoreReader expected data.Severity }{ { name: "missing severity set to Unknown", initialSeverity: "", namespace: "test", reader: &mockReader{}, expected: data.SeverityUnknown, }, { name: "non-cve records metadata missing severity set to Unknown", cveID: "GHSA-1234-1234-1234", initialSeverity: "", namespace: "test", reader: newDeadMockReader(), // should not be used expected: data.SeverityUnknown, }, { name: "non-cve records metadata with severity set should not be overriden", cveID: "GHSA-1234-1234-1234", initialSeverity: "high", namespace: "test", reader: newMockReader("critical"), // should not be used expected: data.SeverityHigh, }, { name: "override empty severity from NVD", initialSeverity: "", namespace: "test", reader: newMockReader("low"), expected: data.SeverityLow, }, { name: "override unknown severity from NVD", initialSeverity: "unknown", namespace: "test", reader: newMockReader("low"), expected: data.SeverityLow, }, { name: "ignore record with severity already set", initialSeverity: "Low", namespace: "test", reader: newMockReader("critical"), // should not be used expected: data.SeverityLow, }, { name: "ignore nvd records", initialSeverity: "Low", namespace: "nvdv2:cves", reader: newDeadMockReader(), // should not be used expected: data.SeverityLow, }, { name: "db errors should not fail or modify the record other than normalizing unset value", initialSeverity: "", namespace: "test", reader: newDeadMockReader(), expected: data.SeverityUnknown, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { record := &db.VulnerabilityMetadata{ ID: "cve-2020-0000", Severity: tt.initialSeverity, Namespace: tt.namespace, } if tt.cveID != "" { record.ID = tt.cveID } normalizeSeverity(record, tt.reader) assert.Equal(t, string(tt.expected), record.Severity) }) } } ================================================ FILE: grype/db/v5/cvss.go ================================================ package v5 import ( "github.com/anchore/grype/grype/vulnerability" ) func NewCvss(m []Cvss) []vulnerability.Cvss { //nolint:prealloc var cvss []vulnerability.Cvss for _, score := range m { cvss = append(cvss, vulnerability.Cvss{ Source: score.Source, Type: score.Type, Version: score.Version, Vector: score.Vector, Metrics: vulnerability.CvssMetrics{ BaseScore: score.Metrics.BaseScore, ExploitabilityScore: score.Metrics.ExploitabilityScore, ImpactScore: score.Metrics.ImpactScore, }, VendorMetadata: score.VendorMetadata, }) } return cvss } ================================================ FILE: grype/db/v5/diff.go ================================================ package v5 type DiffReason = string const ( DiffAdded DiffReason = "added" DiffChanged DiffReason = "changed" DiffRemoved DiffReason = "removed" ) type Diff struct { Reason DiffReason `json:"reason"` ID string `json:"id"` Namespace string `json:"namespace"` Packages []string `json:"packages"` } ================================================ FILE: grype/db/v5/differ/differ.go ================================================ package differ import ( "encoding/json" "fmt" "io" "net/url" "path" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" v5 "github.com/anchore/grype/grype/db/v5" legacyDistribution "github.com/anchore/grype/grype/db/v5/distribution" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" ) type Differ struct { baseCurator legacyDistribution.Curator targetCurator legacyDistribution.Curator } func NewDiffer(config legacyDistribution.Config) (*Differ, error) { baseCurator, err := legacyDistribution.NewCurator(legacyDistribution.Config{ DBRootDir: path.Join(config.DBRootDir, "diff", "base"), ListingURL: config.ListingURL, CACert: config.CACert, ValidateByHashOnGet: config.ValidateByHashOnGet, }) if err != nil { return nil, err } targetCurator, err := legacyDistribution.NewCurator(legacyDistribution.Config{ DBRootDir: path.Join(config.DBRootDir, "diff", "target"), ListingURL: config.ListingURL, CACert: config.CACert, ValidateByHashOnGet: config.ValidateByHashOnGet, }) if err != nil { return nil, err } return &Differ{ baseCurator: baseCurator, targetCurator: targetCurator, }, nil } func (d *Differ) SetBaseDB(base string) error { return d.setOrDownload(&d.baseCurator, base) } func (d *Differ) SetTargetDB(target string) error { return d.setOrDownload(&d.targetCurator, target) } func (d *Differ) setOrDownload(curator *legacyDistribution.Curator, filenameOrURL string) error { u, err := url.ParseRequestURI(filenameOrURL) if err != nil || u.Scheme == "" { *curator, err = legacyDistribution.NewCurator(legacyDistribution.Config{ DBRootDir: filenameOrURL, }) if err != nil { return err } } else { listings, err := d.baseCurator.ListingFromURL() if err != nil { return err } available := listings.Available dbs := available[v5.SchemaVersion] var listing *legacyDistribution.ListingEntry for _, d := range dbs { database := d if *d.URL == *u { listing = &database } } if listing == nil { return fmt.Errorf("unable to find listing for url: %s", filenameOrURL) } if err := download(curator, listing); err != nil { return fmt.Errorf("unable to download vulnerability database: %+v", err) } } return nil } func download(curator *legacyDistribution.Curator, listing *legacyDistribution.ListingEntry) error { // let consumers know of a monitorable event (download + import stages) importProgress := progress.NewManual(1) stage := progress.NewAtomicStage("checking available databases") downloadProgress := progress.NewManual(1) aggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, importProgress) bus.Publish(partybus.Event{ Type: event.UpdateVulnerabilityDatabase, Value: progress.StagedProgressable(&struct { progress.Stager progress.Progressable }{ Stager: progress.Stager(stage), Progressable: progress.Progressable(aggregateProgress), }), }) defer downloadProgress.SetCompleted() defer importProgress.SetCompleted() return curator.UpdateTo(listing, downloadProgress, importProgress, stage) } func (d *Differ) DiffDatabases() (*[]v5.Diff, error) { baseStore, err := d.baseCurator.GetStore() if err != nil { return nil, err } defer log.CloseAndLogError(baseStore, d.baseCurator.Status().Location) targetStore, err := d.targetCurator.GetStore() if err != nil { return nil, err } defer log.CloseAndLogError(targetStore, d.targetCurator.Status().Location) return baseStore.DiffStore(targetStore) } func (d *Differ) DeleteDatabases() error { if err := d.baseCurator.Delete(); err != nil { return fmt.Errorf("unable to delete vulnerability database: %+v", err) } if err := d.targetCurator.Delete(); err != nil { return fmt.Errorf("unable to delete vulnerability database: %+v", err) } return nil } func (d *Differ) Present(outputFormat string, diff *[]v5.Diff, output io.Writer) error { if diff == nil { return nil } switch outputFormat { case "table": rows := [][]string{} for _, d := range *diff { rows = append(rows, []string{d.ID, d.Namespace, d.Reason}) } table := newTable(output, []string{"ID", "Namespace", "Reason"}) if err := table.Bulk(rows); err != nil { return fmt.Errorf("failed to add table rows: %+v", err) } return table.Render() case "json": enc := json.NewEncoder(output) enc.SetEscapeHTML(false) enc.SetIndent("", " ") if err := enc.Encode(*diff); err != nil { return fmt.Errorf("failed to encode diff information: %+v", err) } default: return fmt.Errorf("unsupported output format: %s", outputFormat) } return nil } func newTable(output io.Writer, columns []string) *tablewriter.Table { return tablewriter.NewTable(output, tablewriter.WithHeader(columns), tablewriter.WithHeaderAlignment(tw.AlignLeft), tablewriter.WithHeaderAutoWrap(tw.WrapNone), tablewriter.WithRowAutoWrap(tw.WrapNone), tablewriter.WithAutoHide(tw.On), tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithBehavior( tw.Behavior{ TrimSpace: tw.On, AutoHide: tw.On, }, ), tablewriter.WithPadding( tw.Padding{ Right: " ", }, ), tablewriter.WithRendition( tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleNone), Settings: tw.Settings{ Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, }, }, ), ) } ================================================ FILE: grype/db/v5/differ/differ_test.go ================================================ package differ import ( "bytes" "flag" "strconv" "testing" "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/require" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/distribution" "github.com/anchore/grype/internal/testutils" ) var update = flag.Bool("update", false, "update the *.golden files for diff presenter") func TestNewDiffer(t *testing.T) { //GIVEN config := distribution.Config{} //WHEN differ, err := NewDiffer(config) //THEN require.NoError(t, err) require.NotNil(t, differ.baseCurator) } func Test_DifferDirectory(t *testing.T) { d, err := NewDiffer(distribution.Config{ DBRootDir: "root-dir", }) require.NoError(t, err) err = d.SetBaseDB("testdata/dbs/base") require.NoError(t, err) baseStatus := d.baseCurator.Status() require.Equal(t, "testdata/dbs/base/"+strconv.Itoa(v5.SchemaVersion), baseStatus.Location) err = d.SetTargetDB("testdata/dbs/target") require.NoError(t, err) targetStatus := d.targetCurator.Status() require.Equal(t, "testdata/dbs/target/"+strconv.Itoa(v5.SchemaVersion), targetStatus.Location) } func TestPresent_Json(t *testing.T) { //GIVEN diffs := []v5.Diff{ {v5.DiffAdded, "CVE-1", "nvd", []string{"requests", "vault"}}, {v5.DiffRemoved, "CVE-2", "nvd", []string{"k8s"}}, {v5.DiffChanged, "CVE-3", "nvd", []string{}}, } differ := Differ{} var buffer bytes.Buffer // WHEN require.NoError(t, differ.Present("json", &diffs, &buffer)) //THEN actual := buffer.Bytes() if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) if !bytes.Equal(expected, actual) { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(string(expected), string(actual), true) t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) } } func TestPresent_Table(t *testing.T) { //GIVEN diffs := []v5.Diff{ {v5.DiffAdded, "CVE-1", "nvd", []string{"requests", "vault"}}, {v5.DiffRemoved, "CVE-2", "nvd", []string{"k8s"}}, {v5.DiffChanged, "CVE-3", "nvd", []string{}}, } differ := Differ{} var buffer bytes.Buffer // WHEN require.NoError(t, differ.Present("table", &diffs, &buffer)) //THEN actual := buffer.Bytes() if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) if !bytes.Equal(expected, actual) { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(string(expected), string(actual), true) t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) } } func TestPresent_Invalid(t *testing.T) { //GIVEN diffs := []v5.Diff{ {v5.DiffRemoved, "CVE-2", "nvd", []string{"k8s"}}, } differ := Differ{} var buffer bytes.Buffer // WHEN err := differ.Present("", &diffs, &buffer) //THEN require.Error(t, err) } ================================================ FILE: grype/db/v5/differ/testdata/dbs/base/5/metadata.json ================================================ { "built": "2022-12-18T08:18:18Z", "version": 5, "checksum": "sha256:9d979f7320c575e7ac41c4384e9f55d578cdddd701822563cabc6b913fde7b80" } ================================================ FILE: grype/db/v5/differ/testdata/dbs/target/5/metadata.json ================================================ { "built": "2022-12-18T08:18:18Z", "version": 5, "checksum": "sha256:da2141ae7415abe5cf5390faeed60e164c5a919370ee823c8cc3ad9f4698f56e" } ================================================ FILE: grype/db/v5/differ/testdata/snapshot/TestPresent_Json.golden ================================================ [ { "reason": "added", "id": "CVE-1", "namespace": "nvd", "packages": [ "requests", "vault" ] }, { "reason": "removed", "id": "CVE-2", "namespace": "nvd", "packages": [ "k8s" ] }, { "reason": "changed", "id": "CVE-3", "namespace": "nvd", "packages": [] } ] ================================================ FILE: grype/db/v5/differ/testdata/snapshot/TestPresent_Table.golden ================================================ ID NAMESPACE REASON CVE-1 nvd added CVE-2 nvd removed CVE-3 nvd changed ================================================ FILE: grype/db/v5/distribution/curator.go ================================================ package distribution import ( "context" "crypto/tls" "crypto/x509" "fmt" "io" "net/http" "os" "path" "path/filepath" "strconv" "time" "github.com/hako/durafmt" "github.com/hashicorp/go-cleanhttp" "github.com/mholt/archives" "github.com/spf13/afero" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/clio" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/store" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" ) const ( FileName = v5.VulnerabilityStoreFileName lastUpdateCheckFileName = "last_update_check" ) type Config struct { ID clio.Identification DBRootDir string ListingURL string CACert string ValidateByHashOnGet bool ValidateAge bool MaxAllowedBuiltAge time.Duration RequireUpdateCheck bool ListingFileTimeout time.Duration UpdateTimeout time.Duration UpdateCheckMaxFrequency time.Duration } type Curator struct { fs afero.Fs listingDownloader file.Getter updateDownloader file.Getter targetSchema int dbDir string dbPath string listingURL string validateByHashOnGet bool validateAge bool maxAllowedBuiltAge time.Duration requireUpdateCheck bool updateCheckMaxFrequency time.Duration } func NewCurator(cfg Config) (Curator, error) { dbDir := path.Join(cfg.DBRootDir, strconv.Itoa(v5.SchemaVersion)) fs := afero.NewOsFs() listingClient, err := defaultHTTPClient(fs, cfg.CACert) if err != nil { return Curator{}, err } listingClient.Timeout = cfg.ListingFileTimeout dbClient, err := defaultHTTPClient(fs, cfg.CACert) if err != nil { return Curator{}, err } dbClient.Timeout = cfg.UpdateTimeout return Curator{ fs: fs, targetSchema: v5.SchemaVersion, listingDownloader: file.NewGetter(cfg.ID, listingClient), updateDownloader: file.NewGetter(cfg.ID, dbClient), dbDir: dbDir, dbPath: path.Join(dbDir, FileName), listingURL: cfg.ListingURL, validateByHashOnGet: cfg.ValidateByHashOnGet, validateAge: cfg.ValidateAge, maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge, requireUpdateCheck: cfg.RequireUpdateCheck, updateCheckMaxFrequency: cfg.UpdateCheckMaxFrequency, }, nil } func (c Curator) SupportedSchema() int { return c.targetSchema } func (c *Curator) GetStore() (v5.StoreReader, error) { // ensure the DB is ok _, err := c.validateIntegrity(c.dbDir) if err != nil { return nil, fmt.Errorf("vulnerability database is invalid (run db update to correct): %+v", err) } return store.New(c.dbPath, false) } func (c *Curator) Status() Status { metadata, err := NewMetadataFromDir(c.fs, c.dbDir) if err != nil { return Status{ Err: fmt.Errorf("failed to parse database metadata (%s): %w", c.dbDir, err), } } if metadata == nil { return Status{ Err: fmt.Errorf("database metadata not found at %q", c.dbDir), } } return Status{ Built: metadata.Built, SchemaVersion: metadata.Version, Location: c.dbDir, Checksum: metadata.Checksum, Err: nil, } } // Delete removes the DB and metadata file for this specific schema. func (c *Curator) Delete() error { return c.fs.RemoveAll(c.dbDir) } // Update the existing DB, returning an indication if any action was taken. func (c *Curator) Update() (bool, error) { // nolint: funlen if !c.isUpdateCheckAllowed() { // we should not notify the user of an update check if the current configuration and state // indicates we're should be in a low-pass filter mode (and the check frequency is too high). // this should appear to the user as if we never attempted to check for an update at all. return false, nil } // let consumers know of a monitorable event (download + import stages) importProgress := progress.NewManual(1) stage := progress.NewAtomicStage("checking for update") downloadProgress := progress.NewManual(1) aggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, importProgress) bus.Publish(partybus.Event{ Type: event.UpdateVulnerabilityDatabase, Value: progress.StagedProgressable(&struct { progress.Stager progress.Progressable }{ Stager: progress.Stager(stage), Progressable: progress.Progressable(aggregateProgress), }), }) defer downloadProgress.SetCompleted() defer importProgress.SetCompleted() updateAvailable, metadata, updateEntry, checkErr := c.IsUpdateAvailable() if checkErr != nil { if c.requireUpdateCheck { return false, fmt.Errorf("check for vulnerability database update failed: %w", checkErr) } log.Warnf("unable to check for vulnerability database update") log.Debugf("check for vulnerability update failed: %+v", checkErr) } if updateAvailable { log.Infof("downloading new vulnerability DB") err := c.UpdateTo(updateEntry, downloadProgress, importProgress, stage) if err != nil { return false, fmt.Errorf("unable to update vulnerability database: %w", err) } // only set the last successful update check if the update was successful c.setLastSuccessfulUpdateCheck() if metadata != nil { log.Infof( "updated vulnerability DB from version=%d built=%q to version=%d built=%q", metadata.Version, metadata.Built.String(), updateEntry.Version, updateEntry.Built.String(), ) return true, nil } log.Infof( "downloaded new vulnerability DB version=%d built=%q", updateEntry.Version, updateEntry.Built.String(), ) return true, nil } // there was no update (or any issue while checking for an update) if checkErr == nil { c.setLastSuccessfulUpdateCheck() } stage.Set("no update available") return false, nil } func (c Curator) isUpdateCheckAllowed() bool { if c.updateCheckMaxFrequency == 0 { log.Trace("no max-frequency set for update check") return true } elapsed, err := c.durationSinceUpdateCheck() if err != nil { // we had an IO error (or similar) trying to read or parse the file, we should not block the update check. log.WithFields("error", err).Trace("unable to determine if update check is allowed") return true } if elapsed == nil { // there was no last check (this is a first run case), we should not block the update check. return true } return *elapsed > c.updateCheckMaxFrequency } func (c Curator) durationSinceUpdateCheck() (*time.Duration, error) { // open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp filePath := path.Join(c.dbDir, lastUpdateCheckFileName) if _, err := c.fs.Stat(filePath); os.IsNotExist(err) { log.Trace("first-run of DB update") return nil, nil } fh, err := c.fs.OpenFile(filePath, os.O_RDONLY, 0) if err != nil { return nil, fmt.Errorf("unable to read last update check timestamp: %w", err) } defer fh.Close() // read and parse rfc3339 timestamp var lastCheckStr string _, err = fmt.Fscanf(fh, "%s", &lastCheckStr) if err != nil { return nil, fmt.Errorf("unable to read last update check timestamp: %w", err) } lastCheck, err := time.Parse(time.RFC3339, lastCheckStr) if err != nil { return nil, fmt.Errorf("unable to parse last update check timestamp: %w", err) } if lastCheck.IsZero() { return nil, fmt.Errorf("empty update check timestamp") } elapsed := time.Since(lastCheck) return &elapsed, nil } func (c Curator) setLastSuccessfulUpdateCheck() { // note: we should always assume the DB dir actually exists, otherwise let this operation fail (since having a DB // is a prerequisite for a successful update). filePath := path.Join(c.dbDir, lastUpdateCheckFileName) fh, err := c.fs.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { log.WithFields("error", err).Trace("unable to write last update check timestamp") return } defer fh.Close() _, _ = fmt.Fprintf(fh, "%s", time.Now().UTC().Format(time.RFC3339)) } // IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest listing information // available for this schema. func (c *Curator) IsUpdateAvailable() (bool, *Metadata, *ListingEntry, error) { log.Debugf("checking for available database updates") listing, err := c.ListingFromURL() if err != nil { return false, nil, nil, err } updateEntry := listing.BestUpdate(c.targetSchema) if updateEntry == nil { return false, nil, nil, fmt.Errorf("no db candidates with correct version available (maybe there is an application update available?)") } log.Debugf("found database update candidate: %s", updateEntry) // compare created data to current db date current, err := NewMetadataFromDir(c.fs, c.dbDir) if err != nil { return false, nil, nil, fmt.Errorf("current metadata corrupt: %w", err) } if current.IsSupersededBy(updateEntry) { log.Debugf("database update available: %s", updateEntry) return true, current, updateEntry, nil } log.Debugf("no database update available") return false, nil, nil, nil } // UpdateTo updates the existing DB with the specific other version provided from a listing entry. func (c *Curator) UpdateTo(listing *ListingEntry, downloadProgress, importProgress *progress.Manual, stage *progress.AtomicStage) error { stage.Set("downloading") // note: the temp directory is persisted upon download/validation/activation failure to allow for investigation tempDir, err := c.download(listing, downloadProgress) if err != nil { return err } stage.Set("validating integrity") _, err = c.validateIntegrity(tempDir) if err != nil { return err } stage.Set("importing") err = c.activate(tempDir) if err != nil { return err } stage.Set("updated") importProgress.Set(importProgress.Size()) importProgress.SetCompleted() return c.fs.RemoveAll(tempDir) } // Validate checks the current database to ensure file integrity and if it can be used by this version of the application. func (c *Curator) Validate() error { metadata, err := c.validateIntegrity(c.dbDir) if err != nil { return err } return c.validateStaleness(metadata) } // ImportFrom takes a DB archive file and imports it into the final DB location. func (c *Curator) ImportFrom(dbArchivePath string) error { // note: the temp directory is persisted upon download/validation/activation failure to allow for investigation tempDir, err := os.MkdirTemp("", "grype-import") if err != nil { return fmt.Errorf("unable to create db temp dir: %w", err) } err = unarchive(dbArchivePath, tempDir) if err != nil { return err } _, err = c.validateIntegrity(tempDir) if err != nil { return err } err = c.activate(tempDir) if err != nil { return err } return c.fs.RemoveAll(tempDir) } func (c *Curator) download(listing *ListingEntry, downloadProgress *progress.Manual) (string, error) { tempDir, err := os.MkdirTemp("", "grype-scratch") if err != nil { return "", fmt.Errorf("unable to create db temp dir: %w", err) } // download the db to the temp dir url := listing.URL // from go-getter, adding a checksum as a query string will validate the payload after download // note: the checksum query parameter is not sent to the server query := url.Query() query.Add("checksum", listing.Checksum) url.RawQuery = query.Encode() // go-getter will automatically extract all files within the archive to the temp dir err = c.updateDownloader.GetToDir(tempDir, listing.URL.String(), downloadProgress) if err != nil { return "", fmt.Errorf("unable to download db: %w", err) } return tempDir, nil } // validateStaleness ensures the vulnerability database has not passed // the max allowed age, calculated from the time it was built until now. func (c *Curator) validateStaleness(m Metadata) error { if !c.validateAge { return nil } // built time is defined in UTC, // we should compare it against UTC now := time.Now().UTC() age := now.Sub(m.Built) if age > c.maxAllowedBuiltAge { return fmt.Errorf("the vulnerability database was built %s ago (max allowed age is %s)", durafmt.ParseShort(age), durafmt.ParseShort(c.maxAllowedBuiltAge)) } return nil } func (c *Curator) validateIntegrity(dbDirPath string) (Metadata, error) { // check that the disk checksum still matches the db payload metadata, err := NewMetadataFromDir(c.fs, dbDirPath) if err != nil { return Metadata{}, fmt.Errorf("failed to parse database metadata (%s): %w", dbDirPath, err) } if metadata == nil { return Metadata{}, fmt.Errorf("database metadata not found: %s", dbDirPath) } if c.validateByHashOnGet { dbPath := path.Join(dbDirPath, FileName) valid, actualHash, err := file.ValidateByHash(c.fs, dbPath, metadata.Checksum) if err != nil { return Metadata{}, err } if !valid { return Metadata{}, fmt.Errorf("bad db checksum (%s): %q vs %q", dbPath, metadata.Checksum, actualHash) } } if c.targetSchema != metadata.Version { return Metadata{}, fmt.Errorf("unsupported database version: have=%d want=%d", metadata.Version, c.targetSchema) } // TODO: add version checks here to ensure this version of the application can use this database version (relative to what the DB says, not JUST the metadata!) return *metadata, nil } // activate swaps over the downloaded db to the application directory func (c *Curator) activate(dbDirPath string) error { _, err := c.fs.Stat(c.dbDir) if !os.IsNotExist(err) { // remove any previous databases err = c.Delete() if err != nil { return fmt.Errorf("failed to purge existing database: %w", err) } } // ensure there is an application db directory err = c.fs.MkdirAll(c.dbDir, 0755) if err != nil { return fmt.Errorf("failed to create db directory: %w", err) } // activate the new db cache return file.CopyDir(c.fs, dbDirPath, c.dbDir) } // ListingFromURL loads a Listing from a URL. func (c Curator) ListingFromURL() (Listing, error) { tempFile, err := afero.TempFile(c.fs, "", "grype-db-listing") if err != nil { return Listing{}, fmt.Errorf("unable to create listing temp file: %w", err) } defer func() { log.CloseAndLogError(tempFile, tempFile.Name()) err := c.fs.RemoveAll(tempFile.Name()) if err != nil { log.Errorf("failed to remove file (%s): %w", tempFile.Name(), err) } }() // download the listing file err = c.listingDownloader.GetFile(tempFile.Name(), c.listingURL) if err != nil { return Listing{}, fmt.Errorf("unable to download listing: %w", err) } // parse the listing file listing, err := NewListingFromFile(c.fs, tempFile.Name()) if err != nil { return Listing{}, err } return listing, nil } func defaultHTTPClient(fs afero.Fs, caCertPath string) (*http.Client, error) { httpClient := cleanhttp.DefaultClient() httpClient.Timeout = 30 * time.Second if caCertPath != "" { rootCAs := x509.NewCertPool() pemBytes, err := afero.ReadFile(fs, caCertPath) if err != nil { return nil, fmt.Errorf("unable to configure root CAs for curator: %w", err) } rootCAs.AppendCertsFromPEM(pemBytes) httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: rootCAs, } } return httpClient, nil } func unarchive(source, destination string) error { sourceFile, err := os.Open(source) if err != nil { return err } defer sourceFile.Close() format, stream, err := archives.Identify(context.Background(), source, sourceFile) if err != nil { return err } extractor, ok := format.(archives.Extractor) if !ok { return fmt.Errorf("unable to extract DB file, format not supported: %s", source) } root, err := os.OpenRoot(destination) if err != nil { return err } visitor := func(_ context.Context, file archives.FileInfo) error { if file.IsDir() || file.LinkTarget != "" { return nil } fileReader, err := file.Open() if err != nil { return err } defer fileReader.Close() filename := filepath.Clean(file.NameInArchive) outputFile, err := root.Create(filename) if err != nil { return err } defer outputFile.Close() _, err = io.Copy(outputFile, fileReader) return err } return extractor.Extract(context.Background(), stream, visitor) } ================================================ FILE: grype/db/v5/distribution/curator_test.go ================================================ package distribution import ( "archive/tar" "bufio" "bytes" "compress/gzip" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "path" "path/filepath" "strconv" "strings" "syscall" "testing" "time" "github.com/gookit/color" "github.com/mholt/archives" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/wagoodman/go-progress" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/stringutil" ) type testGetter struct { file map[string]string dir map[string]string calls stringutil.StringSet fs afero.Fs } func newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter { return &testGetter{ file: f, dir: d, calls: stringutil.NewStringSet(), fs: fs, } } // GetFile downloads the give URL into the given path. The URL must reference a single file. func (g *testGetter) GetFile(dst, src string, _ ...*progress.Manual) error { g.calls.Add(src) if _, ok := g.file[src]; !ok { return fmt.Errorf("blerg, no file!") } return afero.WriteFile(g.fs, dst, []byte(g.file[src]), 0755) } // Get downloads the given URL into the given directory. The directory must already exist. func (g *testGetter) GetToDir(dst, src string, _ ...*progress.Manual) error { g.calls.Add(src) if _, ok := g.dir[src]; !ok { return fmt.Errorf("blerg, no file!") } return afero.WriteFile(g.fs, dst, []byte(g.dir[src]), 0755) } func newTestCurator(tb testing.TB, fs afero.Fs, getter file.Getter, dbDir, metadataUrl string, validateDbHash bool) Curator { c, err := NewCurator(Config{ DBRootDir: dbDir, ListingURL: metadataUrl, ValidateByHashOnGet: validateDbHash, }) require.NoError(tb, err) c.listingDownloader = getter c.updateDownloader = getter c.fs = fs return c } func Test_defaultHTTPClientHasCert(t *testing.T) { tests := []struct { name string hasCert bool }{ { name: "no custom cert should use default system root certs", hasCert: false, }, { name: "should use single custom cert", hasCert: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var certPath string if test.hasCert { certPath = generateCertFixture(t) } httpClient, err := defaultHTTPClient(afero.NewOsFs(), certPath) require.NoError(t, err) if test.hasCert { require.NotNil(t, httpClient.Transport.(*http.Transport).TLSClientConfig) assert.Len(t, httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects(), 1) } else { assert.Nil(t, httpClient.Transport.(*http.Transport).TLSClientConfig) } }) } } func Test_defaultHTTPClientTimeout(t *testing.T) { c, err := defaultHTTPClient(afero.NewMemMapFs(), "") require.NoError(t, err) assert.Equal(t, 30*time.Second, c.Timeout) } func generateCertFixture(t *testing.T) string { path := "testdata/tls/server.crt" if _, err := os.Stat(path); !os.IsNotExist(err) { // fixture already exists... return path } t.Log(color.Bold.Sprint("Generating Key/Cert Fixture")) cwd, err := os.Getwd() if err != nil { t.Errorf("unable to get cwd: %+v", err) } cmd := exec.Command("make", "server.crt") cmd.Dir = filepath.Join(cwd, "testdata/tls") stderr, err := cmd.StderrPipe() if err != nil { t.Fatalf("could not get stderr: %+v", err) } stdout, err := cmd.StdoutPipe() if err != nil { t.Fatalf("could not get stdout: %+v", err) } err = cmd.Start() if err != nil { t.Fatalf("failed to start cmd: %+v", err) } show := func(label string, reader io.ReadCloser) { scanner := bufio.NewScanner(reader) scanner.Split(bufio.ScanLines) for scanner.Scan() { t.Logf("%s: %s", label, scanner.Text()) } } go show("out", stdout) go show("err", stderr) if err := cmd.Wait(); err != nil { if exiterr, ok := err.(*exec.ExitError); ok { // The program has exited with an exit code != 0 // This works on both Unix and Windows. Although package // syscall is generally platform dependent, WaitStatus is // defined for both Unix and Windows and in both cases has // an ExitStatus() method with the same signature. if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { if status.ExitStatus() != 0 { t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus()) } } } else { t.Fatalf("unable to get generate fixture result: %+v", err) } } return path } func TestCuratorDownload(t *testing.T) { tests := []struct { name string entry *ListingEntry expectedURL string err bool }{ { name: "download populates returned tempdir", entry: &ListingEntry{ Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), URL: mustUrl(url.Parse("http://a-url/payload.tar.gz")), Checksum: "sha256:deadbeefcafe", }, expectedURL: "http://a-url/payload.tar.gz?checksum=sha256%3Adeadbeefcafe", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { metadataUrl := "http://metadata.io" contents := "CONTENTS!!!" files := map[string]string{} dirs := map[string]string{ test.expectedURL: contents, } fs := afero.NewMemMapFs() getter := newTestGetter(fs, files, dirs) cur := newTestCurator(t, fs, getter, "/tmp/dbdir", metadataUrl, false) path, err := cur.download(test.entry, &progress.Manual{}) if err != nil { t.Fatalf("could not download entry: %+v", err) } if !getter.calls.Contains(test.expectedURL) { t.Fatalf("never made the appropriate fetch call: %+v", getter.calls) } f, err := fs.Open(path) if err != nil { t.Fatalf("no db file: %+v", err) } actual, err := afero.ReadAll(f) if err != nil { t.Fatalf("bad db file read: %+v", err) } if string(actual) != contents { t.Fatalf("bad contents: %+v", string(actual)) } }) } } func TestCuratorValidate(t *testing.T) { tests := []struct { name string fixture string constraint int cfgValidateDbHash bool err bool }{ { name: "good checksum & good constraint", fixture: "testdata/curator-validate/good-checksum", cfgValidateDbHash: true, constraint: 1, err: false, }, { name: "good checksum & bad constraint", fixture: "testdata/curator-validate/good-checksum", cfgValidateDbHash: true, constraint: 2, err: true, }, { name: "bad checksum & good constraint", fixture: "testdata/curator-validate/bad-checksum", cfgValidateDbHash: true, constraint: 1, err: true, }, { name: "bad checksum & bad constraint", fixture: "testdata/curator-validate/bad-checksum", cfgValidateDbHash: true, constraint: 2, err: true, }, { name: "bad checksum ignored on config exception", fixture: "testdata/curator-validate/bad-checksum", cfgValidateDbHash: false, constraint: 1, err: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { metadataUrl := "http://metadata.io" fs := afero.NewOsFs() getter := newTestGetter(fs, nil, nil) cur := newTestCurator(t, fs, getter, "/tmp/dbdir", metadataUrl, test.cfgValidateDbHash) cur.targetSchema = test.constraint md, err := cur.validateIntegrity(test.fixture) if err == nil && test.err { t.Errorf("expected an error but got none") } else if err != nil && !test.err { assert.NotZero(t, md) t.Errorf("expected no error, got: %+v", err) } }) } } func TestCuratorDBPathHasSchemaVersion(t *testing.T) { fs := afero.NewMemMapFs() dbRootPath := "/tmp/dbdir" cur := newTestCurator(t, fs, nil, dbRootPath, "http://metadata.io", false) assert.Equal(t, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), cur.dbDir, "unexpected dir") assert.Contains(t, cur.dbPath, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), "unexpected path") } func TestCurator_validateStaleness(t *testing.T) { type fields struct { validateAge bool maxAllowedDBAge time.Duration md Metadata } now := time.Now().UTC() tests := []struct { name string cur *Curator fields fields wantErr assert.ErrorAssertionFunc }{ { name: "no-validation", fields: fields{ md: Metadata{Built: now}, }, wantErr: assert.NoError, }, { name: "up-to-date", fields: fields{ maxAllowedDBAge: 2 * time.Hour, validateAge: true, md: Metadata{Built: now}, }, wantErr: assert.NoError, }, { name: "stale-data", fields: fields{ maxAllowedDBAge: time.Hour, validateAge: true, md: Metadata{Built: now.UTC().Add(-4 * time.Hour)}, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { return assert.ErrorContains(t, err, "the vulnerability database was built") }, }, { name: "stale-data-no-validation", fields: fields{ maxAllowedDBAge: time.Hour, validateAge: false, md: Metadata{Built: now.Add(-4 * time.Hour)}, }, wantErr: assert.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Curator{ validateAge: tt.fields.validateAge, maxAllowedBuiltAge: tt.fields.maxAllowedDBAge, } tt.wantErr(t, c.validateStaleness(tt.fields.md), fmt.Sprintf("validateStaleness(%v)", tt.fields.md)) }) } } func Test_requireUpdateCheck(t *testing.T) { toJson := func(listing any) []byte { listingContents := bytes.Buffer{} enc := json.NewEncoder(&listingContents) _ = enc.Encode(listing) return listingContents.Bytes() } checksum := func(b []byte) string { h := sha256.New() h.Write(b) return hex.EncodeToString(h.Sum(nil)) } makeTarGz := func(mod time.Time, contents []byte) []byte { metadata := toJson(MetadataJSON{ Built: mod.Format(time.RFC3339), Version: 5, Checksum: "sha256:" + checksum(contents), }) tgz := bytes.Buffer{} gz := gzip.NewWriter(&tgz) w := tar.NewWriter(gz) _ = w.WriteHeader(&tar.Header{ Name: "metadata.json", Size: int64(len(metadata)), Mode: 0600, }) _, _ = w.Write(metadata) _ = w.WriteHeader(&tar.Header{ Name: "vulnerability.db", Size: int64(len(contents)), Mode: 0600, }) _, _ = w.Write(contents) _ = w.Close() _ = gz.Close() return tgz.Bytes() } newTime := time.Date(2024, 06, 13, 17, 13, 13, 0, time.UTC) midTime := time.Date(2022, 06, 13, 17, 13, 13, 0, time.UTC) oldTime := time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC) newDB := makeTarGz(newTime, []byte("some-good-contents")) midMetadata := toJson(MetadataJSON{ Built: midTime.Format(time.RFC3339), Version: 5, Checksum: "sha256:deadbeefcafe", }) var handlerFunc http.HandlerFunc srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handlerFunc(w, r) })) defer srv.Close() newDbURI := "/db.tar.gz" newListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{ Built: newTime, URL: mustUrl(url.Parse(srv.URL + newDbURI)), Checksum: "sha256:" + checksum(newDB), }}}}) oldListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{ Built: oldTime, URL: mustUrl(url.Parse(srv.URL + newDbURI)), Checksum: "sha256:" + checksum(newDB), }}}}) newListingURI := "/listing.json" oldListingURI := "/oldlisting.json" badListingURI := "/badlisting.json" handlerFunc = func(response http.ResponseWriter, request *http.Request) { switch request.RequestURI { case newListingURI: response.WriteHeader(http.StatusOK) _, _ = response.Write(newListing) case oldListingURI: response.WriteHeader(http.StatusOK) _, _ = response.Write(oldListing) case newDbURI: response.WriteHeader(http.StatusOK) _, _ = response.Write(newDB) default: http.Error(response, "not found", http.StatusNotFound) } } tests := []struct { name string config Config dbDir map[string][]byte wantResult bool wantErr require.ErrorAssertionFunc }{ { name: "listing with update", config: Config{ ListingURL: srv.URL + newListingURI, RequireUpdateCheck: true, }, dbDir: map[string][]byte{ "5/metadata.json": midMetadata, }, wantResult: true, wantErr: require.NoError, }, { name: "no update", config: Config{ ListingURL: srv.URL + oldListingURI, RequireUpdateCheck: false, }, dbDir: map[string][]byte{ "5/metadata.json": midMetadata, }, wantResult: false, wantErr: require.NoError, }, { name: "update error fail", config: Config{ ListingURL: srv.URL + badListingURI, RequireUpdateCheck: true, }, wantResult: false, wantErr: require.Error, }, { name: "update error continue", config: Config{ ListingURL: srv.URL + badListingURI, RequireUpdateCheck: false, }, wantResult: false, wantErr: require.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dbTmpDir := t.TempDir() tt.config.DBRootDir = dbTmpDir tt.config.ListingFileTimeout = 1 * time.Minute tt.config.UpdateTimeout = 1 * time.Minute for filePath, contents := range tt.dbDir { fullPath := filepath.Join(dbTmpDir, filepath.FromSlash(filePath)) err := os.MkdirAll(filepath.Dir(fullPath), 0700|os.ModeDir) require.NoError(t, err) err = os.WriteFile(fullPath, contents, 0700) require.NoError(t, err) } c, err := NewCurator(tt.config) require.NoError(t, err) result, err := c.Update() require.Equal(t, tt.wantResult, result) tt.wantErr(t, err) }) } } func TestCuratorTimeoutBehavior(t *testing.T) { failAfter := 10 * time.Second success := make(chan struct{}) errs := make(chan error) timeout := time.After(failAfter) hangForeverHandler := func(w http.ResponseWriter, r *http.Request) { select {} // hang forever } ts := httptest.NewServer(http.HandlerFunc(hangForeverHandler)) cfg := Config{ DBRootDir: "", ListingURL: fmt.Sprintf("%s/listing.json", ts.URL), CACert: "", ValidateByHashOnGet: false, ValidateAge: false, MaxAllowedBuiltAge: 0, ListingFileTimeout: 400 * time.Millisecond, UpdateTimeout: 400 * time.Millisecond, } curator, err := NewCurator(cfg) require.NoError(t, err) u, err := url.Parse(fmt.Sprintf("%s/some-db.tar.gz", ts.URL)) require.NoError(t, err) entry := ListingEntry{ Built: time.Now(), Version: 5, URL: u, Checksum: "83b52a2aa6aff35d208520f40dd36144", } downloadProgress := progress.NewManual(10) importProgress := progress.NewManual(10) stage := progress.NewAtomicStage("some-stage") runTheTest := func(success chan struct{}, errs chan error) { _, _, _, err = curator.IsUpdateAvailable() if err == nil { errs <- errors.New("expected timeout error but got nil") return } if !strings.Contains(err.Error(), "Timeout exceeded") { errs <- fmt.Errorf("expected %q but got %q", "Timeout exceeded", err.Error()) return } err = curator.UpdateTo(&entry, downloadProgress, importProgress, stage) if err == nil { errs <- errors.New("expected timeout error but got nil") return } if !strings.Contains(err.Error(), "Timeout exceeded") { errs <- fmt.Errorf("expected %q but got %q", "Timeout exceeded", err.Error()) return } success <- struct{}{} } go runTheTest(success, errs) select { case <-success: return case err := <-errs: t.Error(err) case <-timeout: t.Fatalf("timeout exceeded (%v)", failAfter) } } func TestCurator_IsUpdateCheckAllowed(t *testing.T) { fs := afero.NewOsFs() tempDir := t.TempDir() curator := Curator{ fs: fs, updateCheckMaxFrequency: 10 * time.Minute, dbDir: tempDir, } writeLastCheckTime := func(t *testing.T, lastCheckTime time.Time) { err := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(lastCheckTime.Format(time.RFC3339)), 0644) require.NoError(t, err) } t.Run("first run check (no last check file)", func(t *testing.T) { require.True(t, curator.isUpdateCheckAllowed()) }) t.Run("check not allowed due to frequency", func(t *testing.T) { writeLastCheckTime(t, time.Now().Add(-5*time.Minute)) require.False(t, curator.isUpdateCheckAllowed()) }) t.Run("check allowed after the frequency period", func(t *testing.T) { writeLastCheckTime(t, time.Now().Add(-20*time.Minute)) require.True(t, curator.isUpdateCheckAllowed()) }) } func TestCurator_DurationSinceUpdateCheck(t *testing.T) { fs := afero.NewOsFs() tempDir := t.TempDir() curator := Curator{ fs: fs, dbDir: tempDir, } writeLastCheckTime := func(t *testing.T, lastCheckTime time.Time) { err := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(lastCheckTime.Format(time.RFC3339)), 0644) require.NoError(t, err) } t.Run("no last check file", func(t *testing.T) { elapsed, err := curator.durationSinceUpdateCheck() require.NoError(t, err) require.Nil(t, elapsed) }) t.Run("last check file does not exist", func(t *testing.T) { // simulate a non-existing file _, err := curator.durationSinceUpdateCheck() require.NoError(t, err) }) t.Run("valid last check file", func(t *testing.T) { writeLastCheckTime(t, time.Now().Add(-5*time.Minute)) elapsed, err := curator.durationSinceUpdateCheck() require.NoError(t, err) require.NotNil(t, elapsed) require.True(t, *elapsed >= 5*time.Minute) }) t.Run("malformed last check file", func(t *testing.T) { err := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte("not a timestamp"), 0644) require.NoError(t, err) _, err = curator.durationSinceUpdateCheck() require.Error(t, err) require.Contains(t, err.Error(), "unable to parse last update check timestamp") }) } func TestCurator_SetLastSuccessfulUpdateCheck(t *testing.T) { fs := afero.NewOsFs() tempDir := t.TempDir() curator := Curator{ fs: fs, dbDir: tempDir, } t.Run("set last successful update check", func(t *testing.T) { curator.setLastSuccessfulUpdateCheck() data, err := afero.ReadFile(fs, path.Join(tempDir, lastUpdateCheckFileName)) require.NoError(t, err) lastCheckTime, err := time.Parse(time.RFC3339, string(data)) require.NoError(t, err) require.WithinDuration(t, time.Now().UTC(), lastCheckTime, time.Second) }) t.Run("error writing last successful update check", func(t *testing.T) { invalidFs := afero.NewReadOnlyFs(fs) // make it read-only, which should simulate a write error curator.fs = invalidFs curator.setLastSuccessfulUpdateCheck() }) } // Mock for the file.Getter interface type MockGetter struct { mock.Mock } func (m *MockGetter) GetFile(dst, src string, monitor ...*progress.Manual) error { args := m.Called(dst, src, monitor) return args.Error(0) } func (m *MockGetter) GetToDir(dst, src string, monitor ...*progress.Manual) error { args := m.Called(dst, src, monitor) return args.Error(0) } func TestCurator_Update_setLastSuccessfulUpdateCheck_notCalled(t *testing.T) { newCurator := func(t *testing.T) *Curator { return &Curator{ fs: afero.NewOsFs(), dbDir: t.TempDir(), updateCheckMaxFrequency: 10 * time.Minute, listingDownloader: &MockGetter{}, updateDownloader: &MockGetter{}, requireUpdateCheck: true, } } t.Run("error checking for update", func(t *testing.T) { c := newCurator(t) c.listingDownloader.(*MockGetter).On("GetFile", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("get listing failed")) _, err := c.Update() require.Error(t, err) require.ErrorContains(t, err, "get listing failed") require.NoFileExists(t, filepath.Join(t.TempDir(), lastUpdateCheckFileName)) }) } func Test_unarchive(t *testing.T) { testFile := filepath.Join(t.TempDir(), "vulnerability.db") f, err := os.Create(testFile) require.NoError(t, err) f.Close() files, err := archives.FilesFromDisk(t.Context(), nil, map[string]string{ testFile: "", }) require.NoError(t, err) source := filepath.Join(t.TempDir(), "archive.tar.zst") out, err := os.Create(source) require.NoError(t, err) format := archives.CompressedArchive{ Compression: archives.Zstd{}, Archival: archives.Tar{}, } err = format.Archive(t.Context(), out, files) require.NoError(t, err) destination := t.TempDir() err = unarchive(source, destination) require.NoError(t, err) expectFile := filepath.Join(destination, "vulnerability.db") require.FileExists(t, expectFile) } ================================================ FILE: grype/db/v5/distribution/listing.go ================================================ package distribution import ( "encoding/json" "fmt" "os" "sort" "github.com/spf13/afero" ) const ListingFileName = "listing.json" // Listing represents the json file which is served up and made available for applications to download and // consume one or more vulnerability db flat files. type Listing struct { Available map[int][]ListingEntry `json:"available"` } // NewListing creates a listing from one or more given ListingEntries. func NewListing(entries ...ListingEntry) Listing { listing := Listing{ Available: make(map[int][]ListingEntry), } for _, entry := range entries { if _, ok := listing.Available[entry.Version]; !ok { listing.Available[entry.Version] = make([]ListingEntry, 0) } listing.Available[entry.Version] = append(listing.Available[entry.Version], entry) } // sort each entry descending by date for idx := range listing.Available { listingEntries := listing.Available[idx] sort.SliceStable(listingEntries, func(i, j int) bool { return listingEntries[i].Built.After(listingEntries[j].Built) }) } return listing } // NewListingFromFile loads a Listing from a given filepath. func NewListingFromFile(fs afero.Fs, path string) (Listing, error) { f, err := fs.Open(path) if err != nil { return Listing{}, fmt.Errorf("unable to open DB listing path: %w", err) } defer f.Close() var l Listing err = json.NewDecoder(f).Decode(&l) if err != nil { return Listing{}, fmt.Errorf("unable to parse DB listing: %w", err) } // sort each entry descending by date for idx := range l.Available { listingEntries := l.Available[idx] sort.SliceStable(listingEntries, func(i, j int) bool { return listingEntries[i].Built.After(listingEntries[j].Built) }) } return l, nil } // BestUpdate returns the ListingEntry from a Listing that meets the given version constraints. func (l *Listing) BestUpdate(targetSchema int) *ListingEntry { if listingEntries, ok := l.Available[targetSchema]; ok { if len(listingEntries) > 0 { return &listingEntries[0] } } return nil } // Write the current listing to the given filepath. func (l Listing) Write(toPath string) error { contents, err := json.MarshalIndent(&l, "", " ") if err != nil { return fmt.Errorf("failed to encode listing file: %w", err) } err = os.WriteFile(toPath, contents, 0600) if err != nil { return fmt.Errorf("failed to write listing file: %w", err) } return nil } ================================================ FILE: grype/db/v5/distribution/listing_entry.go ================================================ package distribution import ( "crypto/sha256" "encoding/json" "fmt" "net/url" "path" "path/filepath" "time" "github.com/spf13/afero" "github.com/anchore/grype/internal/file" ) // ListingEntry represents basic metadata about a database archive such as what is in the archive (built/version) // as well as how to obtain and verify the archive (URL/checksum). type ListingEntry struct { Built time.Time // RFC 3339 Version int URL *url.URL Checksum string } // ListingEntryJSON is a helper struct for converting a ListingEntry into JSON (or parsing from JSON) type ListingEntryJSON struct { Built string `json:"built"` Version int `json:"version"` URL string `json:"url"` Checksum string `json:"checksum"` } // NewListingEntryFromArchive creates a new ListingEntry based on the metadata from a database flat file. func NewListingEntryFromArchive(fs afero.Fs, metadata Metadata, dbArchivePath string, baseURL *url.URL) (ListingEntry, error) { checksum, err := file.HashFile(fs, dbArchivePath, sha256.New()) if err != nil { return ListingEntry{}, fmt.Errorf("unable to find db archive checksum: %w", err) } dbArchiveName := filepath.Base(dbArchivePath) fileURL, _ := url.Parse(baseURL.String()) fileURL.Path = path.Join(fileURL.Path, dbArchiveName) return ListingEntry{ Built: metadata.Built, Version: metadata.Version, URL: fileURL, Checksum: "sha256:" + checksum, }, nil } // ToListingEntry converts a ListingEntryJSON to a ListingEntry. func (l ListingEntryJSON) ToListingEntry() (ListingEntry, error) { build, err := time.Parse(time.RFC3339, l.Built) if err != nil { return ListingEntry{}, fmt.Errorf("cannot convert built time (%s): %+v", l.Built, err) } u, err := url.Parse(l.URL) if err != nil { return ListingEntry{}, fmt.Errorf("cannot parse url (%s): %+v", l.URL, err) } return ListingEntry{ Built: build.UTC(), Version: l.Version, URL: u, Checksum: l.Checksum, }, nil } func (l *ListingEntry) UnmarshalJSON(data []byte) error { var lej ListingEntryJSON if err := json.Unmarshal(data, &lej); err != nil { return err } le, err := lej.ToListingEntry() if err != nil { return err } *l = le return nil } func (l *ListingEntry) MarshalJSON() ([]byte, error) { return json.Marshal(&ListingEntryJSON{ Built: l.Built.Format(time.RFC3339), Version: l.Version, Checksum: l.Checksum, URL: l.URL.String(), }) } func (l ListingEntry) String() string { return fmt.Sprintf("Listing(url=%s)", l.URL) } ================================================ FILE: grype/db/v5/distribution/listing_test.go ================================================ package distribution import ( "net/url" "testing" "time" "github.com/go-test/deep" "github.com/spf13/afero" ) func mustUrl(u *url.URL, err error) *url.URL { if err != nil { panic(err) } return u } func TestNewListingFromPath(t *testing.T) { tests := []struct { fixture string expected Listing err bool }{ { fixture: "testdata/listing.json", expected: Listing{ Available: map[int][]ListingEntry{ 1: { { Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz")), Version: 1, Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e", }, }, 2: { { Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz")), Version: 2, Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8", }, }, }, }, }, { fixture: "testdata/listing-sorted.json", expected: Listing{ Available: map[int][]ListingEntry{ 1: { { Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz")), Version: 1, Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8", }, { Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz")), Version: 1, Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e", }, }, }, }, }, { fixture: "testdata/listing-unsorted.json", expected: Listing{ Available: map[int][]ListingEntry{ 1: { { Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz")), Version: 1, Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8", }, { Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz")), Version: 1, Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e", }, }, }, }, }, } for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { listing, err := NewListingFromFile(afero.NewOsFs(), test.fixture) if err != nil && !test.err { t.Fatalf("failed to get metadata: %+v", err) } else if err == nil && test.err { t.Fatalf("expected errer but got none") } for _, diff := range deep.Equal(listing, test.expected) { t.Errorf("listing difference: %s", diff) } }) } } func TestListingBestUpdate(t *testing.T) { tests := []struct { fixture string constraint int expected *ListingEntry }{ { fixture: "testdata/listing.json", constraint: 2, expected: &ListingEntry{ Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz")), Version: 2, Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8", }, }, { fixture: "testdata/listing.json", constraint: 1, expected: &ListingEntry{ Built: time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC), URL: mustUrl(url.Parse("http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz")), Version: 1, Checksum: "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e", }, }, } for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { listing, err := NewListingFromFile(afero.NewOsFs(), test.fixture) if err != nil { t.Fatalf("failed to get metadata: %+v", err) } actual := listing.BestUpdate(test.constraint) if actual == nil && test.expected != nil || actual != nil && test.expected == nil { t.Fatalf("mismatched best candidate expectations") } for _, diff := range deep.Equal(actual, test.expected) { t.Errorf("listing entry difference: %s", diff) } }) } } ================================================ FILE: grype/db/v5/distribution/metadata.go ================================================ package distribution import ( "encoding/json" "fmt" "os" "path" "time" "github.com/spf13/afero" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" ) const MetadataFileName = "metadata.json" // Metadata represents the basic identifying information of a database flat file (built/version) and a way to // verify the contents (checksum). type Metadata struct { Built time.Time Version int Checksum string } // MetadataJSON is a helper struct for parsing and assembling Metadata objects to and from JSON. type MetadataJSON struct { Built string `json:"built"` // RFC 3339 Version int `json:"version"` Checksum string `json:"checksum"` } // ToMetadata converts a MetadataJSON object to a Metadata object. func (m MetadataJSON) ToMetadata() (Metadata, error) { build, err := time.Parse(time.RFC3339, m.Built) if err != nil { return Metadata{}, fmt.Errorf("cannot convert built time (%s): %+v", m.Built, err) } metadata := Metadata{ Built: build.UTC(), Version: m.Version, Checksum: m.Checksum, } return metadata, nil } func metadataPath(dir string) string { return path.Join(dir, MetadataFileName) } // NewMetadataFromDir generates a Metadata object from a directory containing a vulnerability.db flat file. func NewMetadataFromDir(fs afero.Fs, dir string) (*Metadata, error) { metadataFilePath := metadataPath(dir) exists, err := file.Exists(fs, metadataFilePath) if err != nil { return nil, fmt.Errorf("unable to check if DB metadata path exists (%s): %w", metadataFilePath, err) } if !exists { return nil, nil } f, err := fs.Open(metadataFilePath) if err != nil { return nil, fmt.Errorf("unable to open DB metadata path (%s): %w", metadataFilePath, err) } defer f.Close() var m Metadata err = json.NewDecoder(f).Decode(&m) if err != nil { return nil, fmt.Errorf("unable to parse DB metadata (%s): %w", metadataFilePath, err) } return &m, nil } func (m *Metadata) UnmarshalJSON(data []byte) error { var mj MetadataJSON if err := json.Unmarshal(data, &mj); err != nil { return err } me, err := mj.ToMetadata() if err != nil { return err } *m = me return nil } // IsSupersededBy takes a ListingEntry and determines if the entry candidate is newer than what is hinted at // in the current Metadata object. func (m *Metadata) IsSupersededBy(entry *ListingEntry) bool { if m == nil { log.Debugf("cannot find existing metadata, using update...") // any valid update beats no database, use it! return true } if entry.Version > m.Version { log.Debugf("update is a newer version than the current database, using update...") // the listing is newer than the existing db, use it! return true } if entry.Built.After(m.Built) { log.Debugf("existing database (%s) is older than candidate update (%s), using update...", m.Built.String(), entry.Built.String()) // the listing is newer than the existing db, use it! return true } log.Debugf("existing database is already up to date") return false } func (m Metadata) String() string { return fmt.Sprintf("Metadata(built=%s version=%d checksum=%s)", m.Built, m.Version, m.Checksum) } // Write out a Metadata object to the given path. func (m Metadata) Write(toPath string) error { metadata := MetadataJSON{ Built: m.Built.UTC().Format(time.RFC3339), Version: m.Version, Checksum: m.Checksum, } contents, err := json.MarshalIndent(&metadata, "", " ") if err != nil { return fmt.Errorf("failed to encode metadata file: %w", err) } err = os.WriteFile(toPath, contents, 0600) if err != nil { return fmt.Errorf("failed to write metadata file: %w", err) } return nil } ================================================ FILE: grype/db/v5/distribution/metadata_test.go ================================================ package distribution import ( "testing" "time" "github.com/go-test/deep" "github.com/spf13/afero" ) func TestMetadataParse(t *testing.T) { tests := []struct { fixture string expected *Metadata err bool }{ { fixture: "testdata/metadata-gocase", expected: &Metadata{ Built: time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC), Version: 2, Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8", }, }, { fixture: "testdata/metadata-edt-timezone", expected: &Metadata{ Built: time.Date(2020, 06, 15, 18, 02, 36, 0, time.UTC), Version: 2, Checksum: "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8", }, }, { fixture: "/dev/null/impossible", err: true, }, } for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { metadata, err := NewMetadataFromDir(afero.NewOsFs(), test.fixture) if err != nil && !test.err { t.Fatalf("failed to get metadata: %+v", err) } else if err == nil && test.err { t.Fatalf("expected error but got none") } else if metadata == nil && test.expected != nil { t.Fatalf("metadata not found: %+v", test.fixture) } if metadata != nil && test.expected != nil { for _, diff := range deep.Equal(*metadata, *test.expected) { t.Errorf("metadata difference: %s", diff) } } }) } } func TestMetadataIsSupercededBy(t *testing.T) { tests := []struct { name string current *Metadata update *ListingEntry expectedToSupercede bool }{ { name: "prefer updated versions over later dates", expectedToSupercede: true, current: &Metadata{ Built: time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC), Version: 2, }, update: &ListingEntry{ Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), Version: 3, }, }, { name: "prefer later dates when version is the same", expectedToSupercede: false, current: &Metadata{ Built: time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC), Version: 1, }, update: &ListingEntry{ Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), Version: 1, }, }, { name: "prefer something over nothing", expectedToSupercede: true, current: nil, update: &ListingEntry{ Built: time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC), Version: 1, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := test.current.IsSupersededBy(test.update) if test.expectedToSupercede != actual { t.Errorf("failed supercede assertion: got %+v", actual) } }) } } ================================================ FILE: grype/db/v5/distribution/status.go ================================================ package distribution import "time" type Status struct { Built time.Time `json:"built"` SchemaVersion int `json:"schemaVersion"` Location string `json:"location"` Checksum string `json:"checksum"` Err error `json:"error"` } func (s Status) Status() string { if s.Err != nil { return "invalid" } return "valid" } ================================================ FILE: grype/db/v5/distribution/testdata/curator-validate/bad-checksum/metadata.json ================================================ { "built": "2020-06-15T14:02:36Z", "version": 1, "checksum": "sha256:deadbeefcafe" } ================================================ FILE: grype/db/v5/distribution/testdata/curator-validate/good-checksum/metadata.json ================================================ { "built": "2020-06-15T14:02:36Z", "version": 1, "checksum": "sha256:3baf9c50c94e7f1e65bafac2e6a6d559fb177461dd25bf8fca7e6e9e9c266cb4" } ================================================ FILE: grype/db/v5/distribution/testdata/listing-sorted.json ================================================ { "available": { "1": [ { "built": "2020-06-13T13:13:13-04:00", "version": 1, "url": "http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz", "checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8" }, { "built": "2020-06-12T12:12:12-04:00", "version": 1, "url": "http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz", "checksum": "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e" } ] } } ================================================ FILE: grype/db/v5/distribution/testdata/listing-unsorted.json ================================================ { "available": { "1": [ { "built": "2020-06-12T12:12:12-04:00", "version": 1, "url": "http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz", "checksum": "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e" }, { "built": "2020-06-13T13:13:13-04:00", "version": 1, "url": "http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz", "checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8" } ] } } ================================================ FILE: grype/db/v5/distribution/testdata/listing.json ================================================ { "available": { "1": [ { "built": "2020-06-12T12:12:12-04:00", "version": 1, "url": "http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz", "checksum": "sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e" } ], "2": [ { "built": "2020-06-13T13:13:13-04:00", "version": 2, "url": "http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz", "checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8" } ] } } ================================================ FILE: grype/db/v5/distribution/testdata/metadata-edt-timezone/metadata.json ================================================ { "built": "2020-06-15T14:02:36-04:00", "updated": "2020-06-15T14:02:36-04:00", "last-check": "2020-06-15T14:02:36-04:00", "version": 2, "checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8" } ================================================ FILE: grype/db/v5/distribution/testdata/metadata-gocase/metadata.json ================================================ { "built": "2020-06-15T14:02:36Z", "version": 2, "checksum": "sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8" } ================================================ FILE: grype/db/v5/distribution/testdata/tls/.gitignore ================================================ server.key server.crt www/ listing.json dbdir/ ================================================ FILE: grype/db/v5/distribution/testdata/tls/Makefile ================================================ all: clean serve .PHONY: serve serve: www/listing.json www/db.tar.gz server.crt python3 serve.py .PHONY: grype-test-fail grype-test-fail: clean-dbdir dbdir GRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \ GRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \ go run ../../../../cmd/grype -vv alpine:latest .PHONY: grype-test-pass grype-test-pass: clean-dbdir dbdir GRYPE_DB_CA_CERT=$(shell pwd)/server.crt \ GRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \ GRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \ go run ../../../../cmd/grype -vv alpine:latest dbdir: mkdir -p dbdir server.crt server.key: ./generate-x509-cert-pair.sh www: mkdir -p www listing.json: curl -L -O https://toolbox-data.anchore.io/grype/databases/listing.json www/listing.json www/db.tar.gz: www listing.json $(eval location=$(shell python3 listing.py)) curl -L -o www/db.tar.gz $(location) .PHONY: clean clean: clean-dbdir rm -rf www rm -f server.crt rm -f server.key .PHONY: clean-dbdir clean-dbdir: rm -rf dbdir/ ================================================ FILE: grype/db/v5/distribution/testdata/tls/README.md ================================================ # TLS test utils Note: Makefile, server.crt, and server.key are used in automated testing, the remaining files are for convenience in manual verification. You will require Python 3 to run these utils. To stand up a test server: ``` make serve ``` To test grype against this server: ``` # without the custom cert configured (thus will fail) make grype-test-fail # with the custom cert configured make grype-test-pass ``` To remove all temp files: ``` make clean ``` ================================================ FILE: grype/db/v5/distribution/testdata/tls/generate-x509-cert-pair.sh ================================================ #!/usr/bin/env bash set -eux # we want to still use this on systems where there could be invalid characters in the hostname (e.g. ' or " characters) HOSTNAME=$(hostname | sed "s/['']/'/g" | sed 's/[^a-zA-Z0-9.-]/-/g') # create private key openssl genrsa -out server.key 2048 # generate self-signed public key (cert) based on the private key openssl req -new -x509 -sha256 \ -key server.key \ -out server.crt \ -days 3650 \ -reqexts SAN \ -extensions SAN \ -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:$HOSTNAME.local")) \ -subj "/C=US/ST=Test/L=Test/O=Test/CN=$HOSTNAME.local" ================================================ FILE: grype/db/v5/distribution/testdata/tls/listing.py ================================================ import urllib.request import json import os with open('listing.json', 'r') as fh: data = json.loads(fh.read()) entry = data["available"]["3"][-1] hostname = os.popen('hostname').read().strip() with open('www/listing.json', 'w') as fh: json.dump( { "available": { entry["version"]: [ { "built": entry["built"], "version": entry["version"], "url": f"https://{hostname}.local/db.tar.gz", "checksum": entry["checksum"] } ] } }, fh) print(entry["url"]) ================================================ FILE: grype/db/v5/distribution/testdata/tls/serve.py ================================================ from http.server import HTTPServer, SimpleHTTPRequestHandler import ssl import logging port = 443 directory = "www" class Handler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=directory, **kwargs) def do_GET(self): logging.error(self.headers) SimpleHTTPRequestHandler.do_GET(self) httpd = HTTPServer(('0.0.0.0', port), Handler) sslctx = ssl.SSLContext() sslctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 sslctx.load_cert_chain(certfile='server.crt', keyfile="server.key") httpd.socket = sslctx.wrap_socket(httpd.socket, server_side=True) print(f"Server running on https://0.0.0.0:{port}") httpd.serve_forever() ================================================ FILE: grype/db/v5/fix.go ================================================ package v5 type FixState string const ( UnknownFixState FixState = "unknown" FixedState FixState = "fixed" NotFixedState FixState = "not-fixed" WontFixState FixState = "wont-fix" ) // Fix represents all information about known fixes for a stated vulnerability. type Fix struct { Versions []string `json:"versions"` // The version(s) which this particular vulnerability was fixed in State FixState `json:"state"` } ================================================ FILE: grype/db/v5/id.go ================================================ package v5 import ( "time" ) // ID represents identifying information for a DB and the data it contains. type ID struct { // BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data // contained in the DB, not just when the DB file was created. BuildTimestamp time.Time `json:"build_timestamp"` SchemaVersion int `json:"schema_version"` } type IDReader interface { GetID() (*ID, error) } type IDWriter interface { SetID(ID) error } func NewID(age time.Time) ID { return ID{ BuildTimestamp: age.UTC(), SchemaVersion: SchemaVersion, } } ================================================ FILE: grype/db/v5/match_exclusion_provider.go ================================================ package v5 import ( "fmt" "github.com/anchore/grype/grype/match" ) var _ match.ExclusionProvider = (*MatchExclusionProvider)(nil) type MatchExclusionProvider struct { reader VulnerabilityMatchExclusionStoreReader } func NewMatchExclusionProvider(reader VulnerabilityMatchExclusionStoreReader) *MatchExclusionProvider { return &MatchExclusionProvider{ reader: reader, } } func buildIgnoreRulesFromMatchExclusion(e VulnerabilityMatchExclusion) []match.IgnoreRule { var ignoreRules []match.IgnoreRule if len(e.Constraints) == 0 { ignoreRules = append(ignoreRules, match.IgnoreRule{Vulnerability: e.ID}) return ignoreRules } for _, c := range e.Constraints { ignoreRules = append(ignoreRules, match.IgnoreRule{ Vulnerability: e.ID, Namespace: c.Vulnerability.Namespace, FixState: string(c.Vulnerability.FixState), Package: match.IgnoreRulePackage{ Name: c.Package.Name, Language: c.Package.Language, Type: c.Package.Type, Location: c.Package.Location, Version: c.Package.Version, }, }) } return ignoreRules } func (pr *MatchExclusionProvider) IgnoreRules(vulnerabilityID string) ([]match.IgnoreRule, error) { matchExclusions, err := pr.reader.GetVulnerabilityMatchExclusion(vulnerabilityID) if err != nil { return nil, fmt.Errorf("match exclusion provider failed to fetch records for vulnerability id='%s': %w", vulnerabilityID, err) } var ignoreRules []match.IgnoreRule for _, e := range matchExclusions { rules := buildIgnoreRulesFromMatchExclusion(e) ignoreRules = append(ignoreRules, rules...) } return ignoreRules, nil } ================================================ FILE: grype/db/v5/namespace/cpe/namespace.go ================================================ package cpe import ( "errors" "fmt" "strings" "github.com/anchore/grype/grype/db/v5/pkg/resolver" "github.com/anchore/grype/grype/db/v5/pkg/resolver/stock" ) const ID = "cpe" type Namespace struct { provider string resolver resolver.Resolver } func NewNamespace(provider string) *Namespace { return &Namespace{ provider: provider, resolver: &stock.Resolver{}, } } func FromString(namespaceStr string) (*Namespace, error) { if namespaceStr == "" { return nil, errors.New("unable to create CPE namespace from empty string") } components := strings.Split(namespaceStr, ":") return FromComponents(components) } func FromComponents(components []string) (*Namespace, error) { if len(components) != 2 { return nil, fmt.Errorf("unable to create CPE namespace from %s: incorrect number of components", strings.Join(components, ":")) } if components[1] != ID { return nil, fmt.Errorf("unable to create CPE namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1]) } return NewNamespace(components[0]), nil } func (n *Namespace) Provider() string { return n.provider } func (n *Namespace) Resolver() resolver.Resolver { return n.resolver } func (n Namespace) String() string { return fmt.Sprintf("%s:%s", n.provider, ID) } ================================================ FILE: grype/db/v5/namespace/cpe/namespace_test.go ================================================ package cpe import ( "testing" "github.com/stretchr/testify/assert" ) func TestFromString(t *testing.T) { successTests := []struct { namespaceString string result *Namespace }{ { namespaceString: "abc.xyz:cpe", result: NewNamespace("abc.xyz"), }, } for _, test := range successTests { result, _ := FromString(test.namespaceString) assert.Equal(t, result, test.result) } errorTests := []struct { namespaceString string errorMessage string }{ { namespaceString: "", errorMessage: "unable to create CPE namespace from empty string", }, { namespaceString: "single-component", errorMessage: "unable to create CPE namespace from single-component: incorrect number of components", }, { namespaceString: "too:many:components", errorMessage: "unable to create CPE namespace from too:many:components: incorrect number of components", }, { namespaceString: "wrong:namespace_type", errorMessage: "unable to create CPE namespace from wrong:namespace_type: type namespace_type is incorrect", }, } for _, test := range errorTests { _, err := FromString(test.namespaceString) assert.EqualError(t, err, test.errorMessage) } } ================================================ FILE: grype/db/v5/namespace/distro/namespace.go ================================================ package distro import ( "errors" "fmt" "strings" "github.com/anchore/grype/grype/db/v5/pkg/resolver" "github.com/anchore/grype/grype/db/v5/pkg/resolver/stock" "github.com/anchore/grype/grype/distro" ) const ID = "distro" type Namespace struct { provider string distroType distro.Type version string resolver resolver.Resolver } func NewNamespace(provider string, distroType distro.Type, version string) *Namespace { return &Namespace{ provider: provider, distroType: distroType, version: version, resolver: &stock.Resolver{}, } } func FromString(namespaceStr string) (*Namespace, error) { if namespaceStr == "" { return nil, errors.New("unable to create distro namespace from empty string") } components := strings.Split(namespaceStr, ":") return FromComponents(components) } func FromComponents(components []string) (*Namespace, error) { if len(components) != 4 { return nil, fmt.Errorf("unable to create distro namespace from %s: incorrect number of components", strings.Join(components, ":")) } if components[1] != ID { return nil, fmt.Errorf("unable to create distro namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1]) } return NewNamespace(components[0], distro.Type(components[2]), components[3]), nil } func (n *Namespace) Provider() string { return n.provider } func (n *Namespace) DistroType() distro.Type { return n.distroType } func (n *Namespace) Version() string { return n.version } func (n *Namespace) Resolver() resolver.Resolver { return n.resolver } func (n Namespace) String() string { return fmt.Sprintf("%s:%s:%s:%s", n.provider, ID, n.distroType, n.version) } ================================================ FILE: grype/db/v5/namespace/distro/namespace_test.go ================================================ package distro import ( "testing" "github.com/stretchr/testify/assert" grypeDistro "github.com/anchore/grype/grype/distro" ) func TestFromString(t *testing.T) { successTests := []struct { namespaceString string result *Namespace }{ { namespaceString: "alpine:distro:alpine:3.15", result: NewNamespace("alpine", grypeDistro.Alpine, "3.15"), }, { namespaceString: "redhat:distro:redhat:8", result: NewNamespace("redhat", grypeDistro.RedHat, "8"), }, { namespaceString: "abc.xyz:distro:unknown:abcd~~~", result: NewNamespace("abc.xyz", grypeDistro.Type("unknown"), "abcd~~~"), }, { namespaceString: "msrc:distro:windows:10111", result: NewNamespace("msrc", grypeDistro.Type("windows"), "10111"), }, { namespaceString: "amazon:distro:amazonlinux:2022", result: NewNamespace("amazon", grypeDistro.AmazonLinux, "2022"), }, { namespaceString: "amazon:distro:amazonlinux:2", result: NewNamespace("amazon", grypeDistro.AmazonLinux, "2"), }, { namespaceString: "wolfi:distro:wolfi:rolling", result: NewNamespace("wolfi", grypeDistro.Wolfi, "rolling"), }, { namespaceString: "echo:distro:echo:rolling", result: NewNamespace("echo", grypeDistro.Echo, "rolling"), }, { namespaceString: "minimos:distro:minimos:rolling", result: NewNamespace("minimos", grypeDistro.MinimOS, "rolling"), }, } for _, test := range successTests { result, _ := FromString(test.namespaceString) assert.Equal(t, result, test.result) } errorTests := []struct { namespaceString string errorMessage string }{ { namespaceString: "", errorMessage: "unable to create distro namespace from empty string", }, { namespaceString: "single-component", errorMessage: "unable to create distro namespace from single-component: incorrect number of components", }, { namespaceString: "two:components", errorMessage: "unable to create distro namespace from two:components: incorrect number of components", }, { namespaceString: "still:not:enough", errorMessage: "unable to create distro namespace from still:not:enough: incorrect number of components", }, { namespaceString: "too:many:components:a:b", errorMessage: "unable to create distro namespace from too:many:components:a:b: incorrect number of components", }, { namespaceString: "wrong:namespace_type:a:b", errorMessage: "unable to create distro namespace from wrong:namespace_type:a:b: type namespace_type is incorrect", }, } for _, test := range errorTests { _, err := FromString(test.namespaceString) assert.EqualError(t, err, test.errorMessage) } } ================================================ FILE: grype/db/v5/namespace/from_string.go ================================================ package namespace import ( "errors" "fmt" "strings" "github.com/anchore/grype/grype/db/v5/namespace/cpe" "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/db/v5/namespace/language" ) func FromString(namespaceStr string) (Namespace, error) { if namespaceStr == "" { return nil, errors.New("unable to create namespace from empty string") } components := strings.Split(namespaceStr, ":") if len(components) < 2 { return nil, fmt.Errorf("unable to create namespace from %s: incorrect number of components", namespaceStr) } switch components[1] { case cpe.ID: return cpe.FromComponents(components) case distro.ID: return distro.FromComponents(components) case language.ID: return language.FromComponents(components) default: return nil, fmt.Errorf("unable to create namespace from %s: unknown type %s", namespaceStr, components[1]) } } ================================================ FILE: grype/db/v5/namespace/from_string_test.go ================================================ package namespace import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/db/v5/namespace/cpe" "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/db/v5/namespace/language" grypeDistro "github.com/anchore/grype/grype/distro" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestFromString(t *testing.T) { tests := []struct { namespaceString string result Namespace }{ { namespaceString: "github:language:python", result: language.NewNamespace("github", syftPkg.Python, ""), }, { namespaceString: "github:language:python:python", result: language.NewNamespace("github", syftPkg.Python, syftPkg.PythonPkg), }, { namespaceString: "debian:distro:debian:8", result: distro.NewNamespace("debian", grypeDistro.Debian, "8"), }, { namespaceString: "unknown:distro:amazonlinux:2022.15", result: distro.NewNamespace("unknown", grypeDistro.AmazonLinux, "2022.15"), }, { namespaceString: "ns-1:distro:unknowndistro:abcdefg~~~", result: distro.NewNamespace("ns-1", grypeDistro.Type("unknowndistro"), "abcdefg~~~"), }, { namespaceString: "abc.xyz:cpe", result: cpe.NewNamespace("abc.xyz"), }, } for _, test := range tests { result, _ := FromString(test.namespaceString) assert.Equal(t, result, test.result) } } ================================================ FILE: grype/db/v5/namespace/language/namespace.go ================================================ package language import ( "errors" "fmt" "strings" "github.com/anchore/grype/grype/db/v5/pkg/resolver" syftPkg "github.com/anchore/syft/syft/pkg" ) const ID = "language" type Namespace struct { provider string language syftPkg.Language packageType syftPkg.Type resolver resolver.Resolver } func NewNamespace(provider string, language syftPkg.Language, packageType syftPkg.Type) *Namespace { r, _ := resolver.FromLanguage(language) return &Namespace{ provider: provider, language: language, packageType: packageType, resolver: r, } } func FromString(namespaceStr string) (*Namespace, error) { if namespaceStr == "" { return nil, errors.New("unable to create language namespace from empty string") } components := strings.Split(namespaceStr, ":") return FromComponents(components) } func FromComponents(components []string) (*Namespace, error) { if len(components) != 3 && len(components) != 4 { return nil, fmt.Errorf("unable to create language namespace from %s: incorrect number of components", strings.Join(components, ":")) } if components[1] != ID { return nil, fmt.Errorf("unable to create language namespace from %s: type %s is incorrect", strings.Join(components, ":"), components[1]) } packageType := "" if len(components) == 4 { packageType = components[3] } return NewNamespace(components[0], syftPkg.Language(components[2]), syftPkg.Type(packageType)), nil } func (n *Namespace) Provider() string { return n.provider } func (n *Namespace) Language() syftPkg.Language { return n.language } func (n *Namespace) PackageType() syftPkg.Type { return n.packageType } func (n *Namespace) Resolver() resolver.Resolver { return n.resolver } func (n Namespace) String() string { if n.packageType != "" { return fmt.Sprintf("%s:%s:%s:%s", n.provider, ID, n.language, n.packageType) } return fmt.Sprintf("%s:%s:%s", n.provider, ID, n.language) } ================================================ FILE: grype/db/v5/namespace/language/namespace_test.go ================================================ package language import ( "testing" "github.com/stretchr/testify/assert" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestFromString(t *testing.T) { successTests := []struct { namespaceString string result *Namespace }{ { namespaceString: "github:language:python", result: NewNamespace("github", syftPkg.Python, ""), }, { namespaceString: "github:language:ruby", result: NewNamespace("github", syftPkg.Ruby, ""), }, { namespaceString: "github:language:java", result: NewNamespace("github", syftPkg.Java, ""), }, { namespaceString: "github:language:rust", result: NewNamespace("github", syftPkg.Rust, ""), }, { namespaceString: "abc.xyz:language:something", result: NewNamespace("abc.xyz", syftPkg.Language("something"), ""), }, { namespaceString: "abc.xyz:language:something:another-package-manager", result: NewNamespace("abc.xyz", syftPkg.Language("something"), syftPkg.Type("another-package-manager")), }, } for _, test := range successTests { result, _ := FromString(test.namespaceString) assert.Equal(t, result, test.result) } errorTests := []struct { namespaceString string errorMessage string }{ { namespaceString: "", errorMessage: "unable to create language namespace from empty string", }, { namespaceString: "single-component", errorMessage: "unable to create language namespace from single-component: incorrect number of components", }, { namespaceString: "two:components", errorMessage: "unable to create language namespace from two:components: incorrect number of components", }, { namespaceString: "too:many:components:a:b", errorMessage: "unable to create language namespace from too:many:components:a:b: incorrect number of components", }, { namespaceString: "wrong:namespace_type:a:b", errorMessage: "unable to create language namespace from wrong:namespace_type:a:b: type namespace_type is incorrect", }, } for _, test := range errorTests { _, err := FromString(test.namespaceString) assert.EqualError(t, err, test.errorMessage) } } ================================================ FILE: grype/db/v5/namespace/namespace.go ================================================ package namespace import ( "github.com/anchore/grype/grype/db/v5/pkg/resolver" ) type Namespace interface { Provider() string Resolver() resolver.Resolver String() string } ================================================ FILE: grype/db/v5/pkg/qualifier/from_json.go ================================================ package qualifier import ( "encoding/json" "github.com/go-viper/mapstructure/v2" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" "github.com/anchore/grype/internal/log" ) func FromJSON(data []byte) ([]Qualifier, error) { var records []map[string]interface{} if err := json.Unmarshal(data, &records); err != nil { return nil, err } var qualifiers []Qualifier for _, r := range records { k, ok := r["kind"] if !ok { log.Warn("Skipping qualifier with no kind specified") continue } // create the specific kind of Qualifier switch k { case "rpm-modularity": var q rpmmodularity.Qualifier if err := mapstructure.Decode(r, &q); err != nil { log.Warn("Error decoding rpm-modularity package qualifier: (%v)", err) continue } qualifiers = append(qualifiers, q) case "platform-cpe": var q platformcpe.Qualifier if err := mapstructure.Decode(r, &q); err != nil { log.Warn("Error decoding platform-cpe package qualifier: (%v)", err) continue } qualifiers = append(qualifiers, q) default: log.Debug("Skipping unsupported package qualifier: %s", k) continue } } return qualifiers, nil } ================================================ FILE: grype/db/v5/pkg/qualifier/platformcpe/qualifier.go ================================================ package platformcpe import ( "fmt" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/pkg/qualifier/platformcpe" ) type Qualifier struct { Kind string `json:"kind" mapstructure:"kind"` // Kind of qualifier CPE string `json:"cpe,omitempty" mapstructure:"cpe,omitempty"` // CPE } func (q Qualifier) Parse() qualifier.Qualifier { return platformcpe.New(q.CPE) } func (q Qualifier) String() string { return fmt.Sprintf("kind: %s, cpe: %q", q.Kind, q.CPE) } ================================================ FILE: grype/db/v5/pkg/qualifier/qualifier.go ================================================ package qualifier import ( "fmt" "github.com/anchore/grype/grype/pkg/qualifier" ) type Qualifier interface { fmt.Stringer Parse() qualifier.Qualifier } ================================================ FILE: grype/db/v5/pkg/qualifier/rpmmodularity/qualifier.go ================================================ package rpmmodularity import ( "fmt" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" ) type Qualifier struct { Kind string `json:"kind" mapstructure:"kind"` // Kind of qualifier Module string `json:"module,omitempty" mapstructure:"module,omitempty"` // Modularity label } func (q Qualifier) Parse() qualifier.Qualifier { return rpmmodularity.New(q.Module) } func (q Qualifier) String() string { return fmt.Sprintf("kind: %s, module: %q", q.Kind, q.Module) } ================================================ FILE: grype/db/v5/pkg/resolver/java/resolver.go ================================================ package java import ( "fmt" "strings" grypePkg "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" ) type Resolver struct { } func (r *Resolver) Normalize(name string) string { return strings.ToLower(name) } func (r *Resolver) Resolve(p grypePkg.Package) []string { names := stringutil.NewStringSet() // The current default for the Java ecosystem is to use a Maven-like identifier of the form // ":" if metadata, ok := p.Metadata.(grypePkg.JavaMetadata); ok { if metadata.PomGroupID != "" { if metadata.PomArtifactID != "" { names.Add(r.Normalize(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.PomArtifactID))) } if metadata.ManifestName != "" { names.Add(r.Normalize(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.ManifestName))) } } } if p.PURL != "" { purl, err := packageurl.FromString(p.PURL) if err != nil { log.Warnf("unable to resolve java package identifier from purl=%q: %+v", p.PURL, err) } else { names.Add(r.Normalize(fmt.Sprintf("%s:%s", purl.Namespace, purl.Name))) } } return names.ToSlice() } ================================================ FILE: grype/db/v5/pkg/resolver/java/resolver_test.go ================================================ package java import ( "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" grypePkg "github.com/anchore/grype/grype/pkg" ) func TestResolver_Normalize(t *testing.T) { tests := []struct { name string normalized string }{ { name: "PyYAML", normalized: "pyyaml", }, { name: "oslo.concurrency", normalized: "oslo.concurrency", }, { name: "", normalized: "", }, { name: "test---1", normalized: "test---1", }, { name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ", normalized: "abcd.-__.--.-___.__.--1234____----....xyzzz", }, } resolver := Resolver{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolvedNames := resolver.Normalize(test.name) assert.Equal(t, resolvedNames, test.normalized) }) } } func TestResolver_Resolve(t *testing.T) { tests := []struct { name string pkg grypePkg.Package resolved []string }{ { name: "both artifact and manifest 1", pkg: grypePkg.Package{ Name: "ABCD", Version: "1.2.3.4", Language: "java", Metadata: grypePkg.JavaMetadata{ VirtualPath: "virtual-path-info", PomArtifactID: "pom-ARTIFACT-ID-info", PomGroupID: "pom-group-ID-info", ManifestName: "main-section-name-info", }, }, resolved: []string{"pom-group-id-info:pom-artifact-id-info", "pom-group-id-info:main-section-name-info"}, }, { name: "both artifact and manifest 2", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", PomGroupID: "g-id", ManifestName: "man-name", }, }, resolved: []string{ "g-id:art-id", "g-id:man-name", }, }, { name: "no group id", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", ManifestName: "man-name", }, }, resolved: []string{}, }, { name: "only manifest", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomGroupID: "g-id", ManifestName: "man-name", }, }, resolved: []string{ "g-id:man-name", }, }, { name: "only artifact", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", PomGroupID: "g-id", }, }, resolved: []string{ "g-id:art-id", }, }, { name: "no artifact or manifest", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomGroupID: "g-id", }, }, resolved: []string{}, }, { name: "with valid purl", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", PURL: "pkg:maven/org.anchore/b-name@0.2", }, resolved: []string{"org.anchore:b-name"}, }, { name: "ignore invalid pURLs", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", PURL: "pkg:BAD/", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", PomGroupID: "g-id", }, }, resolved: []string{ "g-id:art-id", }, }, } resolver := Resolver{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolvedNames := resolver.Resolve(test.pkg) assert.ElementsMatch(t, resolvedNames, test.resolved) }) } } ================================================ FILE: grype/db/v5/pkg/resolver/python/resolver.go ================================================ package python import ( "regexp" "strings" grypePkg "github.com/anchore/grype/grype/pkg" ) type Resolver struct { } func (r *Resolver) Normalize(name string) string { // Canonical naming of packages within python is defined by PEP 503 at // https://peps.python.org/pep-0503/#normalized-names, and this code is derived from // the official python implementation of canonical naming at // https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name return strings.ToLower(regexp.MustCompile(`[-_.]+`).ReplaceAllString(name, "-")) } func (r *Resolver) Resolve(p grypePkg.Package) []string { // Canonical naming of packages within python is defined by PEP 503 at // https://peps.python.org/pep-0503/#normalized-names, and this code is derived from // the official python implementation of canonical naming at // https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name return []string{r.Normalize(p.Name)} } ================================================ FILE: grype/db/v5/pkg/resolver/python/resolver_test.go ================================================ package python import ( "testing" "github.com/stretchr/testify/assert" ) func TestResolver_Normalize(t *testing.T) { tests := []struct { name string normalized string }{ { name: "PyYAML", normalized: "pyyaml", }, { name: "oslo.concurrency", normalized: "oslo-concurrency", }, { name: "", normalized: "", }, { name: "test---1", normalized: "test-1", }, { name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ", normalized: "abcd-1234-xyzzz", }, } resolver := Resolver{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolvedNames := resolver.Normalize(test.name) assert.Equal(t, resolvedNames, test.normalized) }) } } ================================================ FILE: grype/db/v5/pkg/resolver/resolver.go ================================================ package resolver import ( "github.com/anchore/grype/grype/db/v5/pkg/resolver/java" "github.com/anchore/grype/grype/db/v5/pkg/resolver/python" "github.com/anchore/grype/grype/db/v5/pkg/resolver/stock" grypePkg "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" ) type Resolver interface { Normalize(string) string Resolve(p grypePkg.Package) []string } func FromLanguage(language syftPkg.Language) (Resolver, error) { var r Resolver switch language { case syftPkg.Python: r = &python.Resolver{} case syftPkg.Java: r = &java.Resolver{} default: r = &stock.Resolver{} } return r, nil } func PackageNames(p grypePkg.Package) []string { names := []string{p.Name} r, _ := FromLanguage(p.Language) if r != nil { parts := r.Resolve(p) if len(parts) > 0 { names = parts } } return names } ================================================ FILE: grype/db/v5/pkg/resolver/resolver_test.go ================================================ package resolver import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/db/v5/pkg/resolver/java" "github.com/anchore/grype/grype/db/v5/pkg/resolver/python" "github.com/anchore/grype/grype/db/v5/pkg/resolver/stock" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestFromLanguage(t *testing.T) { tests := []struct { language syftPkg.Language result Resolver }{ { language: syftPkg.Python, result: &python.Resolver{}, }, { language: syftPkg.Java, result: &java.Resolver{}, }, { language: syftPkg.Ruby, result: &stock.Resolver{}, }, { language: syftPkg.Dart, result: &stock.Resolver{}, }, { language: syftPkg.Rust, result: &stock.Resolver{}, }, { language: syftPkg.Go, result: &stock.Resolver{}, }, { language: syftPkg.JavaScript, result: &stock.Resolver{}, }, { language: syftPkg.Dotnet, result: &stock.Resolver{}, }, { language: syftPkg.PHP, result: &stock.Resolver{}, }, { language: syftPkg.Ruby, result: &stock.Resolver{}, }, { language: syftPkg.Language("something-new"), result: &stock.Resolver{}, }, } for _, test := range tests { result, err := FromLanguage(test.language) assert.NoError(t, err) assert.Equal(t, result, test.result) } } ================================================ FILE: grype/db/v5/pkg/resolver/stock/resolver.go ================================================ package stock import ( "strings" grypePkg "github.com/anchore/grype/grype/pkg" ) type Resolver struct { } func (r *Resolver) Normalize(name string) string { return strings.ToLower(name) } func (r *Resolver) Resolve(p grypePkg.Package) []string { return []string{r.Normalize(p.Name)} } ================================================ FILE: grype/db/v5/pkg/resolver/stock/resolver_test.go ================================================ package stock import ( "testing" "github.com/stretchr/testify/assert" ) func TestResolver_Normalize(t *testing.T) { tests := []struct { packageName string normalized string }{ { packageName: "PyYAML", normalized: "pyyaml", }, { packageName: "oslo.concurrency", normalized: "oslo.concurrency", }, { packageName: "", normalized: "", }, { packageName: "test---1", normalized: "test---1", }, { packageName: "AbCd.-__.--.-___.__.--1234____----....XyZZZ", normalized: "abcd.-__.--.-___.__.--1234____----....xyzzz", }, } resolver := Resolver{} for _, test := range tests { resolvedNames := resolver.Normalize(test.packageName) assert.Equal(t, resolvedNames, test.normalized) } } ================================================ FILE: grype/db/v5/provider_store.go ================================================ package v5 import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/vulnerability" ) type ProviderStore struct { vulnerability.Provider match.ExclusionProvider } ================================================ FILE: grype/db/v5/schema_version.go ================================================ package v5 const SchemaVersion = 5 ================================================ FILE: grype/db/v5/store/diff.go ================================================ package store import ( "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) type storeKey struct { id string namespace string packageName string } type PkgMap = map[storeKey][]string type storeVulnerabilityList struct { items map[storeKey][]storeVulnerability seen bool } type storeVulnerability struct { item *v5.Vulnerability seen bool } type storeMetadata struct { item *v5.VulnerabilityMetadata seen bool } // create manual progress bars for tracking the database diff's progress func trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) { stageProgress := &progress.Manual{} stageProgress.SetTotal(total) differencesDiscovered := &progress.Manual{} stager := &progress.Stage{} bus.Publish(partybus.Event{ Type: event.DatabaseDiffingStarted, Value: monitor.DBDiff{ Stager: stager, StageProgress: progress.Progressable(stageProgress), DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it func buildVulnerabilityPkgsMap(models *[]v5.Vulnerability) *map[storeKey][]string { storeMap := make(map[storeKey][]string) for _, m := range *models { model := m k := getVulnerabilityParentKey(model) if storeVuln, exists := storeMap[k]; exists { storeMap[k] = append(storeVuln, model.PackageName) } else { storeMap[k] = []string{model.PackageName} } } return &storeMap } // creates a diff from the given key using the package maps information to populate // the relevant packages affected by the update func createDiff(baseStore, targetStore *PkgMap, key storeKey, reason v5.DiffReason) *v5.Diff { pkgMap := make(map[string]struct{}) key.packageName = "" if baseStore != nil { if basePkgs, exists := (*baseStore)[key]; exists { for _, pkg := range basePkgs { pkgMap[pkg] = struct{}{} } } } if targetStore != nil { if targetPkgs, exists := (*targetStore)[key]; exists { for _, pkg := range targetPkgs { pkgMap[pkg] = struct{}{} } } } pkgs := []string{} for pkg := range pkgMap { pkgs = append(pkgs, pkg) } return &v5.Diff{ Reason: reason, ID: key.id, Namespace: key.namespace, Packages: pkgs, } } // gets an unpackaged key from a vulnerability func getVulnerabilityParentKey(vuln v5.Vulnerability) storeKey { return storeKey{vuln.ID, vuln.Namespace, ""} } // gets a packaged key from a vulnerability func getVulnerabilityKey(vuln v5.Vulnerability) storeKey { return storeKey{vuln.ID, vuln.Namespace, vuln.PackageName} } type VulnerabilitySet struct { data map[storeKey]*storeVulnerabilityList } func NewVulnerabilitySet(models *[]v5.Vulnerability) *VulnerabilitySet { m := make(map[storeKey]*storeVulnerabilityList, len(*models)) for _, mm := range *models { model := mm parentKey := getVulnerabilityParentKey(model) vulnKey := getVulnerabilityKey(model) if storeVuln, exists := m[parentKey]; exists { if kk, exists := storeVuln.items[vulnKey]; exists { storeVuln.items[vulnKey] = append(kk, storeVulnerability{ item: &model, seen: false, }) } else { storeVuln.items[vulnKey] = []storeVulnerability{{&model, false}} } } else { vuln := storeVulnerabilityList{ items: make(map[storeKey][]storeVulnerability), seen: false, } vuln.items[vulnKey] = []storeVulnerability{{&model, false}} m[parentKey] = &vuln } } return &VulnerabilitySet{ data: m, } } func (v *VulnerabilitySet) in(item v5.Vulnerability) bool { _, exists := v.data[getVulnerabilityParentKey(item)] return exists } func (v *VulnerabilitySet) match(item v5.Vulnerability) bool { if parent, exists := v.data[getVulnerabilityParentKey(item)]; exists { parent.seen = true key := getVulnerabilityKey(item) if children, exists := parent.items[key]; exists { for idx, child := range children { if item.Equal(*child.item) { children[idx].seen = true return true } } } } return false } func (v *VulnerabilitySet) getUnmatched() ([]storeKey, []storeKey) { notSeen := []storeKey{} notEntirelySeen := []storeKey{} for k, item := range v.data { if !item.seen { notSeen = append(notSeen, k) continue } componentLoop: for _, components := range item.items { for _, component := range components { if !component.seen { notEntirelySeen = append(notEntirelySeen, k) break componentLoop } } } } return notSeen, notEntirelySeen } func diffVulnerabilities(baseModels, targetModels *[]v5.Vulnerability, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v5.Diff { diffs := make(map[string]*v5.Diff) m := NewVulnerabilitySet(baseModels) for _, tModel := range *targetModels { targetModel := tModel k := getVulnerabilityKey(targetModel) if m.in(targetModel) { matched := m.match(targetModel) if !matched { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v5.DiffChanged) differentItems.Increment() } } else { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v5.DiffAdded) differentItems.Increment() } } notSeen, partialSeen := m.getUnmatched() for _, k := range partialSeen { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v5.DiffChanged) differentItems.Increment() } for _, k := range notSeen { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v5.DiffRemoved) differentItems.Increment() } return &diffs } type MetadataSet struct { data map[storeKey]*storeMetadata } func NewMetadataSet(models *[]v5.VulnerabilityMetadata) *MetadataSet { m := make(map[storeKey]*storeMetadata, len(*models)) for _, mm := range *models { model := mm m[getMetadataKey(model)] = &storeMetadata{ item: &model, seen: false, } } return &MetadataSet{ data: m, } } func (v *MetadataSet) in(item v5.VulnerabilityMetadata) bool { _, exists := v.data[getMetadataKey(item)] return exists } func (v *MetadataSet) match(item v5.VulnerabilityMetadata) bool { if baseModel, exists := v.data[getMetadataKey(item)]; exists { baseModel.seen = true return baseModel.item.Equal(item) } return false } func (v *MetadataSet) getUnmatched() []storeKey { notSeen := []storeKey{} for k, item := range v.data { if !item.seen { notSeen = append(notSeen, k) } } return notSeen } func diffVulnerabilityMetadata(baseModels, targetModels *[]v5.VulnerabilityMetadata, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v5.Diff { diffs := make(map[string]*v5.Diff) m := NewMetadataSet(baseModels) for _, tModel := range *targetModels { targetModel := tModel k := getMetadataKey(targetModel) if m.in(targetModel) { if !m.match(targetModel) { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v5.DiffChanged) differentItems.Increment() } } else { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v5.DiffAdded) differentItems.Increment() } } for _, k := range m.getUnmatched() { if _, exists := diffs[k.id+k.namespace]; exists { continue } diffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v5.DiffRemoved) differentItems.Increment() } return &diffs } func getMetadataKey(metadata v5.VulnerabilityMetadata) storeKey { return storeKey{metadata.ID, metadata.Namespace, ""} } ================================================ FILE: grype/db/v5/store/diff_test.go ================================================ package store import ( "os" "sort" "testing" "github.com/stretchr/testify/assert" v5 "github.com/anchore/grype/grype/db/v5" ) func Test_GetAllVulnerabilities(t *testing.T) { //GIVEN dbTempFile := t.TempDir() s, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } //WHEN result, err := s.GetAllVulnerabilities() //THEN assert.NotNil(t, result) assert.NoError(t, err) } func Test_GetAllVulnerabilityMetadata(t *testing.T) { //GIVEN dbTempFile := t.TempDir() defer os.Remove(dbTempFile) s, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } //WHEN result, err := s.GetAllVulnerabilityMetadata() //THEN assert.NotNil(t, result) assert.NoError(t, err) } func Test_Diff_Vulnerabilities(t *testing.T) { //GIVEN dbTempFile := t.TempDir() s1, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } dbTempFile = t.TempDir() s2, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } baseVulns := []v5.Vulnerability{ { Namespace: "github:language:python", ID: "CVE-123-4567", PackageName: "pypi:requests", VersionConstraint: "< 2.0 >= 1.29", CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"}, }, { Namespace: "github:language:python", ID: "CVE-123-4567", PackageName: "pypi:requests", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"}, }, { Namespace: "npm", ID: "CVE-123-7654", PackageName: "npm:axios", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"}, Fix: v5.Fix{ State: v5.UnknownFixState, }, }, } targetVulns := []v5.Vulnerability{ { Namespace: "github:language:python", ID: "CVE-123-4567", PackageName: "pypi:requests", VersionConstraint: "< 2.0 >= 1.29", CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"}, }, { Namespace: "github:language:go", ID: "GHSA-....-....", PackageName: "hashicorp:nomad", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*"}, }, { Namespace: "npm", ID: "CVE-123-7654", PackageName: "npm:axios", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"}, Fix: v5.Fix{ State: v5.WontFixState, }, }, } expectedDiffs := []v5.Diff{ { Reason: v5.DiffChanged, ID: "CVE-123-4567", Namespace: "github:language:python", Packages: []string{"pypi:requests"}, }, { Reason: v5.DiffChanged, ID: "CVE-123-7654", Namespace: "npm", Packages: []string{"npm:axios"}, }, { Reason: v5.DiffAdded, ID: "GHSA-....-....", Namespace: "github:language:go", Packages: []string{"hashicorp:nomad"}, }, } for _, vuln := range baseVulns { s1.AddVulnerability(vuln) } for _, vuln := range targetVulns { s2.AddVulnerability(vuln) } //WHEN result, err := s1.DiffStore(s2) sort.SliceStable(*result, func(i, j int) bool { return (*result)[i].ID < (*result)[j].ID }) //THEN assert.NoError(t, err) assert.Equal(t, expectedDiffs, *result) } func Test_Diff_Metadata(t *testing.T) { //GIVEN dbTempFile := t.TempDir() s1, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } dbTempFile = t.TempDir() s2, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } baseVulns := []v5.VulnerabilityMetadata{ { Namespace: "github:language:python", ID: "CVE-123-4567", DataSource: "nvd", }, { Namespace: "github:language:python", ID: "CVE-123-4567", DataSource: "nvd", }, { Namespace: "npm", ID: "CVE-123-7654", DataSource: "nvd", }, } targetVulns := []v5.VulnerabilityMetadata{ { Namespace: "github:language:go", ID: "GHSA-....-....", DataSource: "nvd", }, { Namespace: "npm", ID: "CVE-123-7654", DataSource: "vulndb", }, } expectedDiffs := []v5.Diff{ { Reason: v5.DiffRemoved, ID: "CVE-123-4567", Namespace: "github:language:python", Packages: []string{}, }, { Reason: v5.DiffChanged, ID: "CVE-123-7654", Namespace: "npm", Packages: []string{}, }, { Reason: v5.DiffAdded, ID: "GHSA-....-....", Namespace: "github:language:go", Packages: []string{}, }, } for _, vuln := range baseVulns { s1.AddVulnerabilityMetadata(vuln) } for _, vuln := range targetVulns { s2.AddVulnerabilityMetadata(vuln) } //WHEN result, err := s1.DiffStore(s2) //THEN sort.SliceStable(*result, func(i, j int) bool { return (*result)[i].ID < (*result)[j].ID }) assert.NoError(t, err) assert.Equal(t, expectedDiffs, *result) } ================================================ FILE: grype/db/v5/store/model/id.go ================================================ package model import ( "fmt" "time" v5 "github.com/anchore/grype/grype/db/v5" ) const ( IDTableName = "id" ) type IDModel struct { BuildTimestamp string `gorm:"column:build_timestamp"` SchemaVersion int `gorm:"column:schema_version"` } func NewIDModel(id v5.ID) IDModel { return IDModel{ BuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano), SchemaVersion: id.SchemaVersion, } } func (IDModel) TableName() string { return IDTableName } func (m *IDModel) Inflate() (v5.ID, error) { buildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp) if err != nil { return v5.ID{}, fmt.Errorf("unable to parse build timestamp (%+v): %w", m.BuildTimestamp, err) } return v5.ID{ BuildTimestamp: buildTime, SchemaVersion: m.SchemaVersion, }, nil } ================================================ FILE: grype/db/v5/store/model/vulnerability.go ================================================ package model import ( "encoding/json" "fmt" sqlite "github.com/anchore/grype/grype/db/internal/sqlite" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/pkg/qualifier" ) const ( VulnerabilityTableName = "vulnerability" GetVulnerabilityIndexName = "get_vulnerability_index" ) // VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB. type VulnerabilityModel struct { PK uint64 `gorm:"primary_key;auto_increment;"` ID string `gorm:"column:id"` PackageName string `gorm:"column:package_name; index:get_vulnerability_index"` Namespace string `gorm:"column:namespace; index:get_vulnerability_index"` PackageQualifiers sqlite.NullString `gorm:"column:package_qualifiers"` VersionConstraint string `gorm:"column:version_constraint"` VersionFormat string `gorm:"column:version_format"` CPEs sqlite.NullString `gorm:"column:cpes; default:null"` RelatedVulnerabilities sqlite.NullString `gorm:"column:related_vulnerabilities; default:null"` FixedInVersions sqlite.NullString `gorm:"column:fixed_in_versions; default:null"` FixState string `gorm:"column:fix_state"` Advisories sqlite.NullString `gorm:"column:advisories; default:null"` } // NewVulnerabilityModel generates a new model from a db.Vulnerability struct. func NewVulnerabilityModel(vulnerability v5.Vulnerability) VulnerabilityModel { return VulnerabilityModel{ ID: vulnerability.ID, PackageName: vulnerability.PackageName, Namespace: vulnerability.Namespace, PackageQualifiers: sqlite.ToNullString(vulnerability.PackageQualifiers), VersionConstraint: vulnerability.VersionConstraint, VersionFormat: vulnerability.VersionFormat, FixedInVersions: sqlite.ToNullString(vulnerability.Fix.Versions), FixState: string(vulnerability.Fix.State), Advisories: sqlite.ToNullString(vulnerability.Advisories), CPEs: sqlite.ToNullString(vulnerability.CPEs), RelatedVulnerabilities: sqlite.ToNullString(vulnerability.RelatedVulnerabilities), } } // TableName returns the table which all db.Vulnerability model instances are stored into. func (VulnerabilityModel) TableName() string { return VulnerabilityTableName } // Inflate generates a db.Vulnerability object from the serialized model instance. func (m *VulnerabilityModel) Inflate() (v5.Vulnerability, error) { var cpes []string err := json.Unmarshal(m.CPEs.ToByteSlice(), &cpes) if err != nil { return v5.Vulnerability{}, fmt.Errorf("unable to unmarshal CPEs (%+v): %w", m.CPEs, err) } var related []v5.VulnerabilityReference err = json.Unmarshal(m.RelatedVulnerabilities.ToByteSlice(), &related) if err != nil { return v5.Vulnerability{}, fmt.Errorf("unable to unmarshal related vulnerabilities (%+v): %w", m.RelatedVulnerabilities, err) } var advisories []v5.Advisory err = json.Unmarshal(m.Advisories.ToByteSlice(), &advisories) if err != nil { return v5.Vulnerability{}, fmt.Errorf("unable to unmarshal advisories (%+v): %w", m.Advisories, err) } var versions []string err = json.Unmarshal(m.FixedInVersions.ToByteSlice(), &versions) if err != nil { return v5.Vulnerability{}, fmt.Errorf("unable to unmarshal versions (%+v): %w", m.FixedInVersions, err) } pkgQualifiers, err := qualifier.FromJSON(m.PackageQualifiers.ToByteSlice()) if err != nil { return v5.Vulnerability{}, fmt.Errorf("unable to unmarshal package_qualifiers (%+v): %w", m.PackageQualifiers, err) } return v5.Vulnerability{ ID: m.ID, PackageName: m.PackageName, PackageQualifiers: pkgQualifiers, Namespace: m.Namespace, VersionConstraint: m.VersionConstraint, VersionFormat: m.VersionFormat, CPEs: cpes, RelatedVulnerabilities: related, Fix: v5.Fix{ Versions: versions, State: v5.FixState(m.FixState), }, Advisories: advisories, }, nil } ================================================ FILE: grype/db/v5/store/model/vulnerability_match_exclusion.go ================================================ package model import ( "encoding/json" "fmt" "github.com/anchore/grype/grype/db/internal/sqlite" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/internal/log" ) const ( VulnerabilityMatchExclusionTableName = "vulnerability_match_exclusion" GetVulnerabilityMatchExclusionIndexName = "get_vulnerability_match_exclusion_index" ) // VulnerabilityMatchExclusionModel is a struct used to serialize db.VulnerabilityMatchExclusion information into a sqlite3 DB. type VulnerabilityMatchExclusionModel struct { PK uint64 `gorm:"primary_key;auto_increment;"` ID string `gorm:"column:id; index:get_vulnerability_match_exclusion_index"` Constraints sqlite.NullString `gorm:"column:constraints; default:null"` Justification string `gorm:"column:justification"` } // NewVulnerabilityMatchExclusionModel generates a new model from a db.VulnerabilityMatchExclusion struct. func NewVulnerabilityMatchExclusionModel(v v5.VulnerabilityMatchExclusion) VulnerabilityMatchExclusionModel { return VulnerabilityMatchExclusionModel{ ID: v.ID, Constraints: sqlite.ToNullString(v.Constraints), Justification: v.Justification, } } // TableName returns the table which all db.VulnerabilityMatchExclusion model instances are stored into. func (VulnerabilityMatchExclusionModel) TableName() string { return VulnerabilityMatchExclusionTableName } // Inflate generates a db.VulnerabilityMatchExclusion object from the serialized model instance. func (m *VulnerabilityMatchExclusionModel) Inflate() (*v5.VulnerabilityMatchExclusion, error) { // It's important that we only utilise exclusion constraints that are compatible with this version of Grype, // so if any unknown fields are encountered then ignore that constraint. var constraints []v5.VulnerabilityMatchExclusionConstraint err := json.Unmarshal(m.Constraints.ToByteSlice(), &constraints) if err != nil { return nil, fmt.Errorf("unable to unmarshal vulnerability match exclusion constraints (%+v): %w", m.Constraints, err) } var compatibleConstraints []v5.VulnerabilityMatchExclusionConstraint if len(constraints) > 0 { for _, c := range constraints { if !c.Usable() { log.Debugf("skipping incompatible vulnerability match constraint for vuln id=%s, constraint=%+v", m.ID, c) } else { compatibleConstraints = append(compatibleConstraints, c) } } // If there were constraints and none were compatible, the entire record is not usable by this version of Grype if len(compatibleConstraints) == 0 { return nil, nil } } return &v5.VulnerabilityMatchExclusion{ ID: m.ID, Constraints: compatibleConstraints, Justification: m.Justification, }, nil } ================================================ FILE: grype/db/v5/store/model/vulnerability_match_exclusion_test.go ================================================ package model import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/db/internal/sqlite" v5 "github.com/anchore/grype/grype/db/v5" ) func TestVulnerabilityMatchExclusionModel_Inflate(t *testing.T) { tests := []struct { name string record *VulnerabilityMatchExclusionModel result *v5.VulnerabilityMatchExclusion }{ { name: "Nil constraint", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-12345", Constraints: sqlite.ToNullString(nil), Justification: "Who really knows?", }, result: &v5.VulnerabilityMatchExclusion{ ID: "CVE-12345", Constraints: nil, Justification: "Who really knows?", }, }, { name: "Empty constraint array", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-919", Constraints: sqlite.NewNullString(`[]`, true), Justification: "Always ignore", }, result: &v5.VulnerabilityMatchExclusion{ ID: "CVE-919", Constraints: nil, Justification: "Always ignore", }, }, { name: "Single constraint", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-919", Constraints: sqlite.NewNullString(`[{"vulnerability":{"namespace":"nvd:cpe"},"package":{"language":"python"}}]`, true), Justification: "Python packages are not vulnerable", }, result: &v5.VulnerabilityMatchExclusion{ ID: "CVE-919", Constraints: []v5.VulnerabilityMatchExclusionConstraint{ { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "nvd:cpe", }, Package: v5.PackageExclusionConstraint{ Language: "python", }, }, }, Justification: "Python packages are not vulnerable", }, }, { name: "Single unusable constraint with unknown vulnerability constraint field", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-919", Constraints: sqlite.NewNullString(`[{"vulnerability":{"namespace":"nvd:cpe","something_new":"1234"}}]`, true), Justification: "Python packages are not vulnerable", }, result: nil, }, { name: "Single unusable constraint with unknown package constraint fields", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-919", Constraints: sqlite.NewNullString(`[{"package":{"name":"jim","another_field":"1234","x_y_z":"abc"}}]`, true), Justification: "Python packages are not vulnerable", }, result: nil, }, { name: "Single unusable constraint with unknown root-level constraint fields", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-919", Constraints: sqlite.NewNullString(`[{"x_y_z":{"name":"jim","another_field":"1234","x_y_z":"abc"},"package":{"name":"jim","another_field":"1234","x_y_z":"abc"}}]`, true), Justification: "Python packages are not vulnerable", }, result: nil, }, { name: "Multiple usable constraints", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-2025-152345", Constraints: sqlite.NewNullString(`[{"vulnerability":{"namespace":"abc.xyz:language:ruby","fix_state":"wont-fix"},"package":{"language":"ruby","type":"not-gem"}},{"package":{"language":"python","version":"1000.0.1"}},{"vulnerability":{"namespace":"nvd:cpe"}},{"vulnerability":{"namespace":"nvd:cpe"},"package":{"name":"x"}},{"package":{"location":"/bin/x"}}]`, true), Justification: "Python packages are not vulnerable", }, result: &v5.VulnerabilityMatchExclusion{ ID: "CVE-2025-152345", Constraints: []v5.VulnerabilityMatchExclusionConstraint{ { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "abc.xyz:language:ruby", FixState: "wont-fix", }, Package: v5.PackageExclusionConstraint{ Language: "ruby", Type: "not-gem", }, }, { Package: v5.PackageExclusionConstraint{ Language: "python", Version: "1000.0.1", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "nvd:cpe", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "nvd:cpe", }, Package: v5.PackageExclusionConstraint{ Name: "x", }, }, { Package: v5.PackageExclusionConstraint{ Location: "/bin/x", }, }, }, Justification: "Python packages are not vulnerable", }, }, { name: "Multiple constraints with some unusable", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-2025-152345", Constraints: sqlite.NewNullString(`[{"a_b_c": "x","vulnerability":{"namespace":"abc.xyz:language:ruby","fix_state":"wont-fix"},"package":{"language":"ruby","type":"not-gem"}},{"package":{"language":"python","version":"1000.0.1"}},{"vulnerability":{"namespace":"nvd:cpe"}},{"vulnerability":{"namespace":"nvd:cpe"},"package":{"name":"x"}},{"package":{"location":"/bin/x","nnnn":"no"}}]`, true), Justification: "Python packages are not vulnerable", }, result: &v5.VulnerabilityMatchExclusion{ ID: "CVE-2025-152345", Constraints: []v5.VulnerabilityMatchExclusionConstraint{ { Package: v5.PackageExclusionConstraint{ Language: "python", Version: "1000.0.1", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "nvd:cpe", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "nvd:cpe", }, Package: v5.PackageExclusionConstraint{ Name: "x", }, }, }, Justification: "Python packages are not vulnerable", }, }, { name: "Multiple constraints all unusable", record: &VulnerabilityMatchExclusionModel{ PK: 0, ID: "CVE-2025-152345", Constraints: sqlite.NewNullString(`[{"a_b_c": "x","vulnerability":{"namespace":"abc.xyz:language:ruby","fix_state":"wont-fix"},"package":{"language":"ruby","type":"not-gem"}},{"a_b_c": "x","package":{"language":"python","version":"1000.0.1"}},{"a_b_c": "x","vulnerability":{"namespace":"nvd:cpe"}},{"a_b_c": "x","vulnerability":{"namespace":"nvd:cpe"},"package":{"name":"x"}},{"package":{"location":"/bin/x","nnnn":"no"}}]`, true), Justification: "Python packages are not vulnerable", }, result: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result, err := test.record.Inflate() assert.NoError(t, err) assert.Equal(t, test.result, result) }) } } ================================================ FILE: grype/db/v5/store/model/vulnerability_metadata.go ================================================ package model import ( "encoding/json" "fmt" sqlite "github.com/anchore/grype/grype/db/internal/sqlite" v5 "github.com/anchore/grype/grype/db/v5" ) const ( VulnerabilityMetadataTableName = "vulnerability_metadata" ) // VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB. type VulnerabilityMetadataModel struct { ID string `gorm:"primary_key; column:id;"` Namespace string `gorm:"primary_key; column:namespace;"` DataSource string `gorm:"column:data_source"` RecordSource string `gorm:"column:record_source"` Severity string `gorm:"column:severity"` URLs sqlite.NullString `gorm:"column:urls; default:null"` Description string `gorm:"column:description"` Cvss sqlite.NullString `gorm:"column:cvss; default:null"` } // NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct. func NewVulnerabilityMetadataModel(metadata v5.VulnerabilityMetadata) VulnerabilityMetadataModel { if metadata.Cvss == nil { metadata.Cvss = make([]v5.Cvss, 0) } return VulnerabilityMetadataModel{ ID: metadata.ID, Namespace: metadata.Namespace, DataSource: metadata.DataSource, RecordSource: metadata.RecordSource, Severity: metadata.Severity, URLs: sqlite.ToNullString(metadata.URLs), Description: metadata.Description, Cvss: sqlite.ToNullString(metadata.Cvss), } } // TableName returns the table which all db.VulnerabilityMetadata model instances are stored into. func (VulnerabilityMetadataModel) TableName() string { return VulnerabilityMetadataTableName } // Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance. func (m *VulnerabilityMetadataModel) Inflate() (v5.VulnerabilityMetadata, error) { var links []string var cvss []v5.Cvss if err := json.Unmarshal(m.URLs.ToByteSlice(), &links); err != nil { return v5.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal URLs (%+v): %w", m.URLs, err) } err := json.Unmarshal(m.Cvss.ToByteSlice(), &cvss) if err != nil { return v5.VulnerabilityMetadata{}, fmt.Errorf("unable to unmarshal cvss data (%+v): %w", m.Cvss, err) } return v5.VulnerabilityMetadata{ ID: m.ID, Namespace: m.Namespace, DataSource: m.DataSource, RecordSource: m.RecordSource, Severity: m.Severity, URLs: links, Description: m.Description, Cvss: cvss, }, nil } ================================================ FILE: grype/db/v5/store/model/vulnerability_test.go ================================================ package model import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/db/internal/sqlite" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/pkg/qualifier" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe" "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" ) func TestVulnerabilityModel_Inflate(t *testing.T) { tests := []struct { name string record *VulnerabilityModel result v5.Vulnerability }{ { name: "nil package_qualifiers", record: &VulnerabilityModel{ PK: 0, ID: "CVE-12345", PackageQualifiers: sqlite.ToNullString(nil), }, result: v5.Vulnerability{ ID: "CVE-12345", }, }, { name: "Empty package_qualifiers array", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", }, }, { name: "Single rpmmodularity package qualifier with no module specified", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"kind": "rpm-modularity"}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }, }, }, }, { name: "Single rpmmodularity package qualifier with empty string module specified", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"kind": "rpm-modularity", "module": ""}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }, }, }, }, { name: "Single rpmmodularity package qualifier with module specified", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"kind": "rpm-modularity", "module": "x.y.z:2000"}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "x.y.z:2000", }, }, }, }, { name: "Single platformcpe package qualifier with cpe specified", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"kind": "platform-cpe", "cpe": "cpe:2.3:o:canonical:ubuntu_linux:19.10:*:*:*:*:*:*:*"}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", PackageQualifiers: []qualifier.Qualifier{ platformcpe.Qualifier{ Kind: "platform-cpe", CPE: "cpe:2.3:o:canonical:ubuntu_linux:19.10:*:*:*:*:*:*:*", }, }, }, }, { name: "Single unrecognized package qualifier", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"kind": "unknown", "some-random-slice": [{"x": true}]}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", }, }, { name: "Single package qualifier without kind specified", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"some-random-slice": [{"x": true}]}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", }, }, { name: "Multiple package qualifiers", record: &VulnerabilityModel{ PK: 0, ID: "CVE-919", PackageQualifiers: sqlite.NewNullString(`[{"kind": "rpm-modularity"},{"kind": "rpm-modularity", "module": ""},{"kind": "rpm-modularity", "module": "x.y.z:2000"},{"kind": "unknown", "some-random-slice": [{"x": true}]}]`, true), }, result: v5.Vulnerability{ ID: "CVE-919", PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }, rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "", }, rpmmodularity.Qualifier{ Kind: "rpm-modularity", Module: "x.y.z:2000", }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result, err := test.record.Inflate() assert.NoError(t, err) assert.Equal(t, test.result, result) }) } } ================================================ FILE: grype/db/v5/store/store.go ================================================ package store import ( "fmt" "sort" _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "github.com/go-test/deep" "gorm.io/gorm" "github.com/anchore/grype/grype/db/internal/gormadapter" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/store/model" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" ) // store holds an instance of the database connection type store struct { db *gorm.DB } func models() []any { return []any{ model.IDModel{}, model.VulnerabilityModel{}, model.VulnerabilityMetadataModel{}, model.VulnerabilityMatchExclusionModel{}, } } // New creates a new instance of the store. func New(dbFilePath string, overwrite bool) (v5.Store, error) { db, err := gormadapter.Open(dbFilePath, gormadapter.WithTruncate(overwrite, models(), nil)) if err != nil { return nil, err } return &store{ db: db, }, nil } // GetID fetches the metadata about the databases schema version and build time. func (s *store) GetID() (*v5.ID, error) { var models []model.IDModel result := s.db.Find(&models) if result.Error != nil { return nil, result.Error } switch { case len(models) > 1: return nil, fmt.Errorf("found multiple DB IDs") case len(models) == 1: id, err := models[0].Inflate() if err != nil { return nil, err } return &id, nil } return nil, nil } // SetID stores the databases schema version and build time. func (s *store) SetID(id v5.ID) error { var ids []model.IDModel // replace the existing ID with the given one s.db.Find(&ids).Delete(&ids) m := model.NewIDModel(id) result := s.db.Create(&m) if result.RowsAffected != 1 { return fmt.Errorf("unable to add id (%d rows affected)", result.RowsAffected) } return result.Error } // GetVulnerabilityNamespaces retrieves all possible namespaces from the database. func (s *store) GetVulnerabilityNamespaces() ([]string, error) { var names []string result := s.db.Model(&model.VulnerabilityMetadataModel{}).Distinct().Pluck("namespace", &names) return names, result.Error } // GetVulnerability retrieves vulnerabilities by namespace and id func (s *store) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) { var models []model.VulnerabilityModel query := s.db.Where("id = ?", id) if namespace != "" { query = query.Where("namespace = ?", namespace) } result := query.Find(&models) vulnerabilities := make([]v5.Vulnerability, len(models)) for idx, m := range models { vulnerability, err := m.Inflate() if err != nil { return nil, err } vulnerabilities[idx] = vulnerability } return vulnerabilities, result.Error } // SearchForVulnerabilities retrieves vulnerabilities by namespace and package func (s *store) SearchForVulnerabilities(namespace, packageName string) ([]v5.Vulnerability, error) { var models []model.VulnerabilityModel result := s.db.Where("namespace = ? AND package_name = ?", namespace, packageName).Find(&models) vulnerabilities := make([]v5.Vulnerability, len(models)) for idx, m := range models { vulnerability, err := m.Inflate() if err != nil { return nil, err } vulnerabilities[idx] = vulnerability } return vulnerabilities, result.Error } // AddVulnerability saves one or more vulnerabilities into the sqlite3 store. func (s *store) AddVulnerability(vulnerabilities ...v5.Vulnerability) error { for _, vulnerability := range vulnerabilities { m := model.NewVulnerabilityModel(vulnerability) result := s.db.Create(&m) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return fmt.Errorf("unable to add vulnerability (%d rows affected)", result.RowsAffected) } } return nil } // GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source. func (s *store) GetVulnerabilityMetadata(id, namespace string) (*v5.VulnerabilityMetadata, error) { var models []model.VulnerabilityMetadataModel result := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, Namespace: namespace}).Find(&models) if result.Error != nil { return nil, result.Error } switch { case len(models) > 1: return nil, fmt.Errorf("found multiple metadatas for single ID=%q Namespace=%q", id, namespace) case len(models) == 1: metadata, err := models[0].Inflate() if err != nil { return nil, err } return &metadata, nil } return nil, nil } // AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB. // //nolint:gocognit func (s *store) AddVulnerabilityMetadata(metadata ...v5.VulnerabilityMetadata) error { for _, m := range metadata { existing, err := s.GetVulnerabilityMetadata(m.ID, m.Namespace) if err != nil { return fmt.Errorf("failed to verify existing entry: %w", err) } if existing != nil { // merge with the existing entry switch { case existing.Severity != m.Severity: return fmt.Errorf("existing metadata has mismatched severity (%q!=%q)", existing.Severity, m.Severity) case existing.Description != m.Description: return fmt.Errorf("existing metadata has mismatched description (%q!=%q)", existing.Description, m.Description) } incoming: // go through all incoming CVSS and see if they are already stored. // If they exist already in the database then skip adding them, // preventing a duplicate for _, incomingCvss := range m.Cvss { for _, existingCvss := range existing.Cvss { if len(deep.Equal(incomingCvss, existingCvss)) == 0 { // duplicate found, so incoming CVSS shouldn't get added continue incoming } } // a duplicate CVSS entry wasn't found, so append the incoming CVSS existing.Cvss = append(existing.Cvss, incomingCvss) } links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } existing.URLs = links.ToSlice() sort.Strings(existing.URLs) newModel := model.NewVulnerabilityMetadataModel(*existing) result := s.db.Save(&newModel) if result.RowsAffected != 1 { return fmt.Errorf("unable to merge vulnerability metadata (%d rows affected)", result.RowsAffected) } if result.Error != nil { return result.Error } } else { // this is a new entry newModel := model.NewVulnerabilityMetadataModel(m) result := s.db.Create(&newModel) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return fmt.Errorf("unable to add vulnerability metadata (%d rows affected)", result.RowsAffected) } } } return nil } // GetVulnerabilityMatchExclusion retrieves one or more vulnerability match exclusion records given a vulnerability identifier. func (s *store) GetVulnerabilityMatchExclusion(id string) ([]v5.VulnerabilityMatchExclusion, error) { var models []model.VulnerabilityMatchExclusionModel result := s.db.Where("id = ?", id).Find(&models) var exclusions []v5.VulnerabilityMatchExclusion for _, m := range models { exclusion, err := m.Inflate() if err != nil { return nil, err } if exclusion != nil { exclusions = append(exclusions, *exclusion) } } return exclusions, result.Error } // AddVulnerabilityMatchExclusion saves one or more vulnerability match exclusion records into the sqlite3 store. func (s *store) AddVulnerabilityMatchExclusion(exclusions ...v5.VulnerabilityMatchExclusion) error { for _, exclusion := range exclusions { m := model.NewVulnerabilityMatchExclusionModel(exclusion) result := s.db.Create(&m) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return fmt.Errorf("unable to add vulnerability match exclusion (%d rows affected)", result.RowsAffected) } } return nil } func (s *store) Close() error { log.Debug("optimizing database settings for memory-efficient VACUUM") // Reduce memory footprint for VACUUM operation memoryEfficientStatements := []string{ "PRAGMA cache_size = -32768", // 32MB instead of 1GB "PRAGMA temp_store = FILE", // Use disk for temp storage "PRAGMA mmap_size = 67108864", // 64MB instead of 1GB "PRAGMA journal_mode = TRUNCATE", // Disk-based journal, no directory modifications } for _, stmt := range memoryEfficientStatements { if err := s.db.Exec(stmt).Error; err != nil { log.WithFields("statement", stmt, "error", err).Warn("failed to apply memory optimization") } else { log.WithFields("statement", stmt).Debug("applied memory optimization") } } log.Debug("starting database VACUUM operation") s.db.Exec("VACUUM;") log.Debug("database VACUUM operation completed") sqlDB, _ := s.db.DB() if sqlDB != nil { _ = sqlDB.Close() } return nil } // GetAllVulnerabilities gets all vulnerabilities in the database func (s *store) GetAllVulnerabilities() (*[]v5.Vulnerability, error) { var models []model.VulnerabilityModel if result := s.db.Find(&models); result.Error != nil { return nil, result.Error } vulns := make([]v5.Vulnerability, len(models)) for idx, m := range models { vuln, err := m.Inflate() if err != nil { return nil, err } vulns[idx] = vuln } return &vulns, nil } // GetAllVulnerabilityMetadata gets all vulnerability metadata in the database func (s *store) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, error) { var models []model.VulnerabilityMetadataModel if result := s.db.Find(&models); result.Error != nil { return nil, result.Error } metadata := make([]v5.VulnerabilityMetadata, len(models)) for idx, m := range models { data, err := m.Inflate() if err != nil { return nil, err } metadata[idx] = data } return &metadata, nil } // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v5.StoreReader) (*[]v5.Diff, error) { // 7 stages, one for each step of the diff process (stages) rowsProgress, diffItems, stager := trackDiff(7) stager.Current = "reading target vulnerabilities" targetVulns, err := targetStore.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } stager.Current = "reading base vulnerabilities" baseVulns, err := s.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } stager.Current = "preparing" baseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns) targetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns) stager.Current = "comparing vulnerabilities" allDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems) stager.Current = "reading base metadata" baseMetadata, err := s.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() stager.Current = "reading target metadata" targetMetadata, err := targetStore.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() stager.Current = "comparing metadata" metaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems) for k, diff := range *metaDiffsMap { (*allDiffsMap)[k] = diff } allDiffs := []v5.Diff{} for _, diff := range *allDiffsMap { allDiffs = append(allDiffs, *diff) } rowsProgress.SetCompleted() diffItems.SetCompleted() return &allDiffs, nil } ================================================ FILE: grype/db/v5/store/store_test.go ================================================ package store import ( "encoding/json" "sort" "testing" "time" "github.com/go-test/deep" "github.com/stretchr/testify/assert" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/store/model" ) func assertIDReader(t *testing.T, reader v5.IDReader, expected v5.ID) { t.Helper() if actual, err := reader.GetID(); err != nil { t.Fatalf("failed to get ID: %+v", err) } else { diffs := deep.Equal(&expected, actual) if len(diffs) > 0 { for _, d := range diffs { t.Errorf("Diff: %+v", d) } } } } func TestStore_GetID_SetID(t *testing.T) { dbTempFile := t.TempDir() s, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } expected := v5.ID{ BuildTimestamp: time.Now().UTC(), SchemaVersion: 2, } if err = s.SetID(expected); err != nil { t.Fatalf("failed to set ID: %+v", err) } assertIDReader(t, s, expected) } func assertVulnerabilityReader(t *testing.T, reader v5.VulnerabilityStoreReader, namespace, name string, expected []v5.Vulnerability) { if actual, err := reader.SearchForVulnerabilities(namespace, name); err != nil { t.Fatalf("failed to get Vulnerability: %+v", err) } else { if len(actual) != len(expected) { t.Fatalf("unexpected number of vulns: %d", len(actual)) } for idx := range actual { diffs := deep.Equal(expected[idx], actual[idx]) if len(diffs) > 0 { for _, d := range diffs { t.Errorf("Diff: %+v", d) } } } } } func TestStore_GetVulnerability_SetVulnerability(t *testing.T) { dbTempFile := t.TempDir() s, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } extra := []v5.Vulnerability{ { ID: "my-cve-33333", PackageName: "package-name-2", Namespace: "my-namespace", VersionConstraint: "< 1.0", VersionFormat: "semver", CPEs: []string{"a-cool-cpe"}, RelatedVulnerabilities: []v5.VulnerabilityReference{ { ID: "another-cve", Namespace: "nvd", }, { ID: "an-other-cve", Namespace: "nvd", }, }, Fix: v5.Fix{ Versions: []string{"2.0.1"}, State: v5.FixedState, }, }, { ID: "my-other-cve-33333", PackageName: "package-name-3", Namespace: "my-namespace", VersionConstraint: "< 509.2.2", VersionFormat: "semver", CPEs: []string{"a-cool-cpe"}, RelatedVulnerabilities: []v5.VulnerabilityReference{ { ID: "another-cve", Namespace: "nvd", }, { ID: "an-other-cve", Namespace: "nvd", }, }, Fix: v5.Fix{ State: v5.NotFixedState, }, }, } expected := []v5.Vulnerability{ { ID: "my-cve", PackageName: "package-name", Namespace: "my-namespace", VersionConstraint: "< 1.0", VersionFormat: "semver", CPEs: []string{"a-cool-cpe"}, RelatedVulnerabilities: []v5.VulnerabilityReference{ { ID: "another-cve", Namespace: "nvd", }, { ID: "an-other-cve", Namespace: "nvd", }, }, Fix: v5.Fix{ Versions: []string{"1.0.1"}, State: v5.FixedState, }, }, { ID: "my-other-cve", PackageName: "package-name", Namespace: "my-namespace", VersionConstraint: "< 509.2.2", VersionFormat: "semver", CPEs: nil, RelatedVulnerabilities: []v5.VulnerabilityReference{ { ID: "another-cve", Namespace: "nvd", }, { ID: "an-other-cve", Namespace: "nvd", }, }, Fix: v5.Fix{ Versions: []string{"4.0.5"}, State: v5.FixedState, }, }, { ID: "yet-another-cve", PackageName: "package-name", Namespace: "my-namespace", VersionConstraint: "< 1000.0.0", VersionFormat: "semver", CPEs: nil, RelatedVulnerabilities: nil, Fix: v5.Fix{ Versions: []string{"1000.0.1"}, State: v5.FixedState, }, }, { ID: "yet-another-cve-with-advisories", PackageName: "package-name", Namespace: "my-namespace", VersionConstraint: "< 1000.0.0", VersionFormat: "semver", CPEs: nil, RelatedVulnerabilities: nil, Fix: v5.Fix{ Versions: []string{"1000.0.1"}, State: v5.FixedState, }, Advisories: []v5.Advisory{{ID: "ABC-12345", Link: "https://abc.xyz"}}, }, } total := append(expected, extra...) if err = s.AddVulnerability(total...); err != nil { t.Fatalf("failed to set Vulnerability: %+v", err) } var allEntries []model.VulnerabilityModel s.(*store).db.Find(&allEntries) if len(allEntries) != len(total) { t.Fatalf("unexpected number of entries: %d", len(allEntries)) } assertVulnerabilityReader(t, s, expected[0].Namespace, expected[0].PackageName, expected) } func assertVulnerabilityMetadataReader(t *testing.T, reader v5.VulnerabilityMetadataStoreReader, id, namespace string, expected v5.VulnerabilityMetadata) { if actual, err := reader.GetVulnerabilityMetadata(id, namespace); err != nil { t.Fatalf("failed to get metadata: %+v", err) } else if actual == nil { t.Fatalf("no metadata returned for id=%q namespace=%q", id, namespace) } else { sortMetadataCvss(actual.Cvss) sortMetadataCvss(expected.Cvss) // make sure they both have the same number of CVSS entries - preventing a panic on later assertions assert.Len(t, expected.Cvss, len(actual.Cvss)) for idx, actualCvss := range actual.Cvss { assert.Equal(t, actualCvss.Vector, expected.Cvss[idx].Vector) assert.Equal(t, actualCvss.Version, expected.Cvss[idx].Version) assert.Equal(t, actualCvss.Metrics, expected.Cvss[idx].Metrics) actualVendor, err := json.Marshal(actualCvss.VendorMetadata) if err != nil { t.Errorf("unable to marshal vendor metadata: %q", err) } expectedVendor, err := json.Marshal(expected.Cvss[idx].VendorMetadata) if err != nil { t.Errorf("unable to marshal vendor metadata: %q", err) } assert.Equal(t, string(actualVendor), string(expectedVendor)) } // nil the Cvss field because it is an interface - verification of Cvss // has already happened at this point expected.Cvss = nil actual.Cvss = nil assert.Equal(t, &expected, actual) } } func sortMetadataCvss(cvss []v5.Cvss) { sort.Slice(cvss, func(i, j int) bool { // first, sort by Vector if cvss[i].Vector > cvss[j].Vector { return true } if cvss[i].Vector < cvss[j].Vector { return false } // then try to sort by BaseScore if Vector is the same return cvss[i].Metrics.BaseScore < cvss[j].Metrics.BaseScore }) } // CustomMetadata is effectively a noop, its values aren't meaningful and are // mostly useful to ensure that any type can be stored and then retrieved for // assertion in these test cases where custom vendor CVSS scores are used type CustomMetadata struct { SuperScore string Vendor string } func TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) { dbTempFile := t.TempDir() s, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } total := []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { VendorMetadata: CustomMetadata{ Vendor: "redhat", SuperScore: "1000", }, Version: "2.0", Metrics: v5.NewCvssMetrics( 1.1, 2.2, 3.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.3, 2.1, 3.2, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE", VendorMetadata: nil, }, }, }, { ID: "my-other-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, } if err = s.AddVulnerabilityMetadata(total...); err != nil { t.Fatalf("failed to set metadata: %+v", err) } var allEntries []model.VulnerabilityMetadataModel s.(*store).db.Find(&allEntries) if len(allEntries) != len(total) { t.Fatalf("unexpected number of entries: %d", len(allEntries)) } } func TestStore_MergeVulnerabilityMetadata(t *testing.T) { tests := []struct { name string add []v5.VulnerabilityMetadata expected v5.VulnerabilityMetadata err bool }{ { name: "go-case", add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, { name: "merge-links", add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://google.com"}, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://yahoo.com"}, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re", "https://google.com", "https://yahoo.com"}, Cvss: []v5.Cvss{}, }, }, { name: "bad-severity", add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "meh, push that for next tuesday...", URLs: []string{"https://redhat.com"}, }, }, err: true, }, { name: "mismatch-description", err: true, add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, }, { name: "mismatch-cvss2", err: false, add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:P--VERY", }, }, }, }, { name: "mismatch-cvss3", err: false, add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 0, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "best description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 0, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { dbTempDir := t.TempDir() s, err := New(dbTempDir, true) if err != nil { t.Fatalf("could not create store: %+v", err) } // add each metadata in order var theErr error for _, metadata := range test.add { err = s.AddVulnerabilityMetadata(metadata) if err != nil { theErr = err break } } if test.err && theErr == nil { t.Fatalf("expected error but did not get one") } else if !test.err && theErr != nil { t.Fatalf("expected no error but got one: %+v", theErr) } else if test.err && theErr != nil { // test pass... return } // ensure there is exactly one entry var allEntries []model.VulnerabilityMetadataModel s.(*store).db.Find(&allEntries) if len(allEntries) != 1 { t.Fatalf("unexpected number of entries: %d", len(allEntries)) } // get the resulting metadata object if actual, err := s.GetVulnerabilityMetadata(test.expected.ID, test.expected.Namespace); err != nil { t.Fatalf("failed to get metadata: %+v", err) } else { diffs := deep.Equal(&test.expected, actual) if len(diffs) > 0 { for _, d := range diffs { t.Errorf("Diff: %+v", d) } } } }) } } func TestCvssScoresInMetadata(t *testing.T) { tests := []struct { name string add []v5.VulnerabilityMetadata expected v5.VulnerabilityMetadata }{ { name: "append-cvss", add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, }, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, { name: "append-vendor-cvss", add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, }, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", VendorMetadata: CustomMetadata{ SuperScore: "100", Vendor: "debian", }, }, }, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", }, { Version: "2.0", Metrics: v5.NewCvssMetrics( 4.1, 5.2, 6.3, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY", VendorMetadata: CustomMetadata{ SuperScore: "100", Vendor: "debian", }, }, }, }, }, { name: "avoids-duplicate-cvss", add: []v5.VulnerabilityMetadata{ { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, { ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, expected: v5.VulnerabilityMetadata{ ID: "my-cve", RecordSource: "record-source", Namespace: "namespace", Severity: "pretty bad", URLs: []string{"https://ancho.re"}, Description: "worst description ever", Cvss: []v5.Cvss{ { Version: "3.0", Metrics: v5.NewCvssMetrics( 1.4, 2.5, 3.6, ), Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD", }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { dbTempDir := t.TempDir() s, err := New(dbTempDir, true) if err != nil { t.Fatalf("could not create s: %+v", err) } // add each metadata in order for _, metadata := range test.add { err = s.AddVulnerabilityMetadata(metadata) if err != nil { t.Fatalf("unable to s vulnerability metadata: %+v", err) } } // ensure there is exactly one entry var allEntries []model.VulnerabilityMetadataModel s.(*store).db.Find(&allEntries) if len(allEntries) != 1 { t.Fatalf("unexpected number of entries: %d", len(allEntries)) } assertVulnerabilityMetadataReader(t, s, test.expected.ID, test.expected.Namespace, test.expected) }) } } func assertVulnerabilityMatchExclusionReader(t *testing.T, reader v5.VulnerabilityMatchExclusionStoreReader, id string, expected []v5.VulnerabilityMatchExclusion) { if actual, err := reader.GetVulnerabilityMatchExclusion(id); err != nil { t.Fatalf("failed to get Vulnerability Match Exclusion: %+v", err) } else { t.Logf("%+v", actual) if len(actual) != len(expected) { t.Fatalf("unexpected number of vulnerability match exclusions: expected=%d, actual=%d", len(expected), len(actual)) } for idx := range actual { diffs := deep.Equal(expected[idx], actual[idx]) if len(diffs) > 0 { for _, d := range diffs { t.Errorf("Diff: %+v", d) } } } } } func TestStore_GetVulnerabilityMatchExclusion_SetVulnerabilityMatchExclusion(t *testing.T) { dbTempFile := t.TempDir() s, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } extra := []v5.VulnerabilityMatchExclusion{ { ID: "CVE-1234-14567", Constraints: []v5.VulnerabilityMatchExclusionConstraint{ { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "extra-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Name: "abc", Language: "ruby", Version: "1.2.3", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "extra-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Name: "abc", Language: "ruby", Version: "4.5.6", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "extra-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Name: "time-1", Language: "ruby", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "extra-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Name: "abc.xyz:nothing-of-interest", Type: "java-archive", }, }, }, Justification: "Because I said so.", }, { ID: "CVE-1234-10", Constraints: nil, Justification: "Because I said so.", }, } expected := []v5.VulnerabilityMatchExclusion{ { ID: "CVE-1234-9999999", Constraints: []v5.VulnerabilityMatchExclusionConstraint{ { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "old-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Language: "python", Name: "abc", Version: "1.2.3", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "old-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Language: "python", Name: "abc", Version: "4.5.6", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "old-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Language: "python", Name: "time-245", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "old-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Type: "npm", Name: "everything", }, }, }, Justification: "This is a false positive", }, { ID: "CVE-1234-9999999", Constraints: []v5.VulnerabilityMatchExclusionConstraint{ { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "old-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Language: "go", Type: "go-module", Name: "abc", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ Namespace: "some-other-namespace:cpe", }, Package: v5.PackageExclusionConstraint{ Language: "go", Type: "go-module", Name: "abc", }, }, { Vulnerability: v5.VulnerabilityExclusionConstraint{ FixState: "wont-fix", }, }, }, Justification: "This is also a false positive", }, { ID: "CVE-1234-9999999", Justification: "global exclude", }, } total := append(expected, extra...) if err = s.AddVulnerabilityMatchExclusion(total...); err != nil { t.Fatalf("failed to set Vulnerability Match Exclusion: %+v", err) } var allEntries []model.VulnerabilityMatchExclusionModel s.(*store).db.Find(&allEntries) if len(allEntries) != len(total) { t.Fatalf("unexpected number of entries: %d", len(allEntries)) } assertVulnerabilityMatchExclusionReader(t, s, expected[0].ID, expected) } func Test_DiffStore(t *testing.T) { //GIVEN dbTempFile := t.TempDir() s1, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } dbTempFile = t.TempDir() s2, err := New(dbTempFile, true) if err != nil { t.Fatalf("could not create store: %+v", err) } baseVulns := []v5.Vulnerability{ { Namespace: "github:language:python", ID: "CVE-123-4567", PackageName: "pypi:requests", VersionConstraint: "< 2.0 >= 1.29", CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"}, }, { Namespace: "github:language:python", ID: "CVE-123-4567", PackageName: "pypi:requests", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"}, }, { Namespace: "npm", ID: "CVE-123-7654", PackageName: "npm:axios", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"}, Fix: v5.Fix{ State: v5.UnknownFixState, }, }, { Namespace: "nuget", ID: "GHSA-****-******", PackageName: "nuget:net", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:nuget:net:*:*:*:*:*:*"}, Fix: v5.Fix{ State: v5.UnknownFixState, }, }, { Namespace: "hex", ID: "GHSA-^^^^-^^^^^^", PackageName: "hex:esbuild", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:hex:esbuild:*:*:*:*:*:*"}, }, } baseMetadata := []v5.VulnerabilityMetadata{ { Namespace: "nuget", ID: "GHSA-****-******", DataSource: "nvd", }, } targetVulns := []v5.Vulnerability{ { Namespace: "github:language:python", ID: "CVE-123-4567", PackageName: "pypi:requests", VersionConstraint: "< 2.0 >= 1.29", CPEs: []string{"cpe:2.3:pypi:requests:*:*:*:*:*:*"}, }, { Namespace: "github:language:go", ID: "GHSA-....-....", PackageName: "hashicorp:nomad", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*"}, }, { Namespace: "github:language:go", ID: "GHSA-....-....", PackageName: "hashicorp:n", VersionConstraint: "< 2.0 >= 1.17", CPEs: []string{"cpe:2.3:golang:hashicorp:n:*:*:*:*:*"}, }, { Namespace: "npm", ID: "CVE-123-7654", PackageName: "npm:axios", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:npm:axios:*:*:*:*:*:*"}, Fix: v5.Fix{ State: v5.WontFixState, }, }, { Namespace: "nuget", ID: "GHSA-****-******", PackageName: "nuget:net", VersionConstraint: "< 3.0 >= 2.17", CPEs: []string{"cpe:2.3:nuget:net:*:*:*:*:*:*"}, Fix: v5.Fix{ State: v5.UnknownFixState, }, }, } expectedDiffs := []v5.Diff{ { Reason: v5.DiffChanged, ID: "CVE-123-4567", Namespace: "github:language:python", Packages: []string{"pypi:requests"}, }, { Reason: v5.DiffChanged, ID: "CVE-123-7654", Namespace: "npm", Packages: []string{"npm:axios"}, }, { Reason: v5.DiffRemoved, ID: "GHSA-****-******", Namespace: "nuget", Packages: []string{"nuget:net"}, }, { Reason: v5.DiffAdded, ID: "GHSA-....-....", Namespace: "github:language:go", Packages: []string{"hashicorp:n", "hashicorp:nomad"}, }, { Reason: v5.DiffRemoved, ID: "GHSA-^^^^-^^^^^^", Namespace: "hex", Packages: []string{"hex:esbuild"}, }, } for _, vuln := range baseVulns { s1.AddVulnerability(vuln) } for _, vuln := range targetVulns { s2.AddVulnerability(vuln) } for _, meta := range baseMetadata { s1.AddVulnerabilityMetadata(meta) } //WHEN result, err := s1.DiffStore(s2) //THEN sort.SliceStable(*result, func(i, j int) bool { return (*result)[i].ID < (*result)[j].ID }) for i := range *result { sort.Strings((*result)[i].Packages) } assert.NoError(t, err) assert.Equal(t, expectedDiffs, *result) } ================================================ FILE: grype/db/v5/store.go ================================================ package v5 import "io" type Store interface { StoreReader StoreWriter } type StoreReader interface { IDReader DiffReader VulnerabilityStoreReader VulnerabilityMetadataStoreReader VulnerabilityMatchExclusionStoreReader io.Closer } type StoreWriter interface { IDWriter VulnerabilityStoreWriter VulnerabilityMetadataStoreWriter VulnerabilityMatchExclusionStoreWriter io.Closer } type DiffReader interface { DiffStore(s StoreReader) (*[]Diff, error) } ================================================ FILE: grype/db/v5/vulnerability.go ================================================ package v5 import ( "fmt" "sort" "strings" qualifierV5 "github.com/anchore/grype/grype/db/v5/pkg/qualifier" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) // Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE. type Vulnerability struct { ID string `json:"id"` // The identifier of the vulnerability or advisory PackageName string `json:"package_name"` // The name of the package that is vulnerable Namespace string `json:"namespace"` // The ecosystem where the package resides PackageQualifiers []qualifierV5.Qualifier `json:"package_qualifiers"` // The qualifiers for determining if a package is vulnerable VersionConstraint string `json:"version_constraint"` // The version range which the given package is vulnerable VersionFormat string `json:"version_format"` // The format which all version fields should be interpreted as CPEs []string `json:"cpes"` // The CPEs which are considered vulnerable RelatedVulnerabilities []VulnerabilityReference `json:"related_vulnerabilities"` // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record) Fix Fix `json:"fix"` // All information about fixed versions Advisories []Advisory `json:"advisories"` // Any vendor advisories about fixes or other notifications about this vulnerability } type VulnerabilityReference struct { ID string `json:"id"` Namespace string `json:"namespace"` } //nolint:gocognit func (v *Vulnerability) Equal(vv Vulnerability) bool { equal := v.ID == vv.ID && v.PackageName == vv.PackageName && v.Namespace == vv.Namespace && len(v.PackageQualifiers) == len(vv.PackageQualifiers) && v.VersionConstraint == vv.VersionConstraint && v.VersionFormat == vv.VersionFormat && len(v.CPEs) == len(vv.CPEs) && len(v.RelatedVulnerabilities) == len(vv.RelatedVulnerabilities) && len(v.Advisories) == len(vv.Advisories) && v.Fix.State == vv.Fix.State && len(v.Fix.Versions) == len(vv.Fix.Versions) if !equal { return false } sort.Strings(v.CPEs) sort.Strings(vv.CPEs) for idx, cpe := range v.CPEs { if cpe != vv.CPEs[idx] { return false } } sortedBaseRelVulns, sortedTargetRelVulns := sortRelatedVulns(v.RelatedVulnerabilities), sortRelatedVulns(vv.RelatedVulnerabilities) for idx, item := range sortedBaseRelVulns { if item != sortedTargetRelVulns[idx] { return false } } sortedBaseAdvisories, sortedTargetAdvisories := sortAdvisories(v.Advisories), sortAdvisories(vv.Advisories) for idx, item := range sortedBaseAdvisories { if item != sortedTargetAdvisories[idx] { return false } } sortedBasePkgQualifiers, sortedTargetPkgQualifiers := sortPackageQualifiers(v.PackageQualifiers), sortPackageQualifiers(vv.PackageQualifiers) for idx, item := range sortedBasePkgQualifiers { if item != sortedTargetPkgQualifiers[idx] { return false } } sort.Strings(v.Fix.Versions) sort.Strings(vv.Fix.Versions) for idx, item := range v.Fix.Versions { if item != vv.Fix.Versions[idx] { return false } } return true } func sortRelatedVulns(vulns []VulnerabilityReference) []VulnerabilityReference { sort.SliceStable(vulns, func(i, j int) bool { b1, b2 := strings.Builder{}, strings.Builder{} b1.WriteString(vulns[i].ID) b1.WriteString(vulns[i].Namespace) b2.WriteString(vulns[j].ID) b2.WriteString(vulns[j].Namespace) return b1.String() < b2.String() }) return vulns } func sortAdvisories(advisories []Advisory) []Advisory { sort.SliceStable(advisories, func(i, j int) bool { b1, b2 := strings.Builder{}, strings.Builder{} b1.WriteString(advisories[i].ID) b1.WriteString(advisories[i].Link) b2.WriteString(advisories[j].ID) b2.WriteString(advisories[j].Link) return b1.String() < b2.String() }) return advisories } func sortPackageQualifiers(qualifiers []qualifierV5.Qualifier) []qualifierV5.Qualifier { sort.SliceStable(qualifiers, func(i, j int) bool { return qualifiers[i].String() < qualifiers[j].String() }) return qualifiers } func NewVulnerability(vuln Vulnerability) (*vulnerability.Vulnerability, error) { format := version.ParseFormat(vuln.VersionFormat) constraint, err := version.GetConstraint(vuln.VersionConstraint, format) if err != nil { return nil, fmt.Errorf("failed to parse constraint='%s' format='%s': %w", vuln.VersionConstraint, format, err) } pkgQualifiers := make([]qualifier.Qualifier, len(vuln.PackageQualifiers)) for idx, q := range vuln.PackageQualifiers { pkgQualifiers[idx] = q.Parse() } advisories := make([]vulnerability.Advisory, len(vuln.Advisories)) for idx, advisory := range vuln.Advisories { advisories[idx] = vulnerability.Advisory{ ID: advisory.ID, Link: advisory.Link, } } var relatedVulnerabilities []vulnerability.Reference for _, r := range vuln.RelatedVulnerabilities { relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{ ID: r.ID, Namespace: r.Namespace, }) } var cpes []cpe.CPE for _, cp := range vuln.CPEs { c, err := cpe.New(cp, "") if err != nil { log.WithFields("err", err, "cpe", cp).Debug("failed to parse CPE") continue } cpes = append(cpes, c) } return &vulnerability.Vulnerability{ PackageName: vuln.PackageName, Constraint: constraint, Reference: vulnerability.Reference{ ID: vuln.ID, Namespace: vuln.Namespace, }, CPEs: cpes, PackageQualifiers: pkgQualifiers, Fix: vulnerability.Fix{ Versions: vuln.Fix.Versions, State: vulnerability.FixState(vuln.Fix.State), }, Advisories: advisories, RelatedVulnerabilities: relatedVulnerabilities, }, nil } ================================================ FILE: grype/db/v5/vulnerability_match_exclusion.go ================================================ package v5 import ( "encoding/json" ) // VulnerabilityMatchExclusion represents the minimum data fields necessary to automatically filter certain // vulnerabilities from match results based on the specified constraints. type VulnerabilityMatchExclusion struct { ID string `json:"id"` // The identifier of the vulnerability or advisory Constraints []VulnerabilityMatchExclusionConstraint `json:"constraints,omitempty"` // The constraints under which the exclusion applies Justification string `json:"justification"` // Justification for the exclusion } // VulnerabilityMatchExclusionConstraint describes criteria for which matches should be excluded type VulnerabilityMatchExclusionConstraint struct { Vulnerability VulnerabilityExclusionConstraint `json:"vulnerability,omitempty"` // Vulnerability exclusion criteria Package PackageExclusionConstraint `json:"package,omitempty"` // Package exclusion criteria ExtraFields map[string]interface{} `json:"-"` } func (c VulnerabilityMatchExclusionConstraint) Usable() bool { return len(c.ExtraFields) == 0 && c.Vulnerability.Usable() && c.Package.Usable() } func (c *VulnerabilityMatchExclusionConstraint) UnmarshalJSON(data []byte) error { // Create a new type from the target type to avoid recursion. type _vulnerabilityMatchExclusionConstraint VulnerabilityMatchExclusionConstraint // Unmarshal into an instance of the new type. var _c _vulnerabilityMatchExclusionConstraint if err := json.Unmarshal(data, &_c); err != nil { return err } if err := json.Unmarshal(data, &_c.ExtraFields); err != nil { return err } delete(_c.ExtraFields, "vulnerability") delete(_c.ExtraFields, "package") if len(_c.ExtraFields) == 0 { _c.ExtraFields = nil } // Cast the new type instance to the original type and assign. *c = VulnerabilityMatchExclusionConstraint(_c) return nil } // VulnerabilityExclusionConstraint describes criteria for excluding a match based on additional vulnerability components type VulnerabilityExclusionConstraint struct { Namespace string `json:"namespace,omitempty"` // Vulnerability namespace FixState FixState `json:"fix_state,omitempty"` // Vulnerability fix state ExtraFields map[string]interface{} `json:"-"` } func (v VulnerabilityExclusionConstraint) Usable() bool { return len(v.ExtraFields) == 0 } func (v *VulnerabilityExclusionConstraint) UnmarshalJSON(data []byte) error { // Create a new type from the target type to avoid recursion. type _vulnerabilityExclusionConstraint VulnerabilityExclusionConstraint // Unmarshal into an instance of the new type. var _v _vulnerabilityExclusionConstraint if err := json.Unmarshal(data, &_v); err != nil { return err } if err := json.Unmarshal(data, &_v.ExtraFields); err != nil { return err } delete(_v.ExtraFields, "namespace") delete(_v.ExtraFields, "fix_state") if len(_v.ExtraFields) == 0 { _v.ExtraFields = nil } // Cast the new type instance to the original type and assign. *v = VulnerabilityExclusionConstraint(_v) return nil } // PackageExclusionConstraint describes criteria for excluding a match based on package components type PackageExclusionConstraint struct { Name string `json:"name,omitempty"` // Package name Language string `json:"language,omitempty"` // The language ecosystem for a package Type string `json:"type,omitempty"` // Package type Version string `json:"version,omitempty"` // Package version Location string `json:"location,omitempty"` // Package location ExtraFields map[string]interface{} `json:"-"` } func (p PackageExclusionConstraint) Usable() bool { return len(p.ExtraFields) == 0 } func (p *PackageExclusionConstraint) UnmarshalJSON(data []byte) error { // Create a new type from the target type to avoid recursion. type _packageExclusionConstraint PackageExclusionConstraint // Unmarshal into an instance of the new type. var _p _packageExclusionConstraint if err := json.Unmarshal(data, &_p); err != nil { return err } if err := json.Unmarshal(data, &_p.ExtraFields); err != nil { return err } delete(_p.ExtraFields, "name") delete(_p.ExtraFields, "language") delete(_p.ExtraFields, "type") delete(_p.ExtraFields, "version") delete(_p.ExtraFields, "location") if len(_p.ExtraFields) == 0 { _p.ExtraFields = nil } // Cast the new type instance to the original type and assign. *p = PackageExclusionConstraint(_p) return nil } ================================================ FILE: grype/db/v5/vulnerability_match_exclusion_store.go ================================================ package v5 type VulnerabilityMatchExclusionStore interface { VulnerabilityMatchExclusionStoreReader VulnerabilityMatchExclusionStoreWriter } type VulnerabilityMatchExclusionStoreReader interface { GetVulnerabilityMatchExclusion(id string) ([]VulnerabilityMatchExclusion, error) } type VulnerabilityMatchExclusionStoreWriter interface { AddVulnerabilityMatchExclusion(exclusion ...VulnerabilityMatchExclusion) error } ================================================ FILE: grype/db/v5/vulnerability_metadata.go ================================================ package v5 import ( "reflect" "github.com/anchore/grype/grype/vulnerability" ) // VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching. type VulnerabilityMetadata struct { ID string `json:"id"` // The identifier of the vulnerability or advisory Namespace string `json:"namespace"` // Where this entry is valid within DataSource string `json:"data_source"` // A URL where the data was sourced from RecordSource string `json:"record_source"` // The source of the vulnerability information (relative to the immediate upstream in the enterprise feedgroup) Severity string `json:"severity"` // How severe the vulnerability is (valid values are defined by upstream sources currently) URLs []string `json:"urls"` // URLs to get more information about the vulnerability or advisory Description string `json:"description"` // Description of the vulnerability Cvss []Cvss `json:"cvss"` // Common Vulnerability Scoring System values } // Cvss contains select Common Vulnerability Scoring System fields for a vulnerability. type Cvss struct { // VendorMetadata captures non-standard CVSS fields that vendors can sometimes // include when providing CVSS information. This vendor-specific metadata type // allows to capture that data for persisting into the database VendorMetadata interface{} `json:"vendor_metadata"` Metrics CvssMetrics `json:"metrics"` Vector string `json:"vector"` // A textual representation of the metric values used to determine the score Version string `json:"version"` // The version of the CVSS spec, for example 2.0, 3.0, or 3.1 Source string `json:"source"` // Identifies the organization that provided the score Type string `json:"type"` // Whether the source is a `primary` or `secondary` source } // CvssMetrics are the quantitative values that make up a CVSS score. type CvssMetrics struct { // BaseScore ranges from 0 - 10 and defines qualities intrinsic to the severity of a vulnerability. BaseScore float64 `json:"base_score"` // ExploitabilityScore is a pointer to avoid having a 0 value by default. // It is an indicator of how easy it may be for an attacker to exploit // a vulnerability ExploitabilityScore *float64 `json:"exploitability_score"` // ImpactScore represents the effects of an exploited vulnerability // relative to compromise in confidentiality, integrity, and availability. // It is an optional parameter, so that is why it is a pointer instead of // a regular field ImpactScore *float64 `json:"impact_score"` } func NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMetrics { return CvssMetrics{ BaseScore: baseScore, ExploitabilityScore: &exploitabilityScore, ImpactScore: &impactScore, } } func (v *VulnerabilityMetadata) Equal(vv VulnerabilityMetadata) bool { equal := v.ID == vv.ID && v.Namespace == vv.Namespace && v.DataSource == vv.DataSource && v.RecordSource == vv.RecordSource && v.Severity == vv.Severity && v.Description == vv.Description && len(v.URLs) == len(vv.URLs) && len(v.Cvss) == len(vv.Cvss) if !equal { return false } for idx, cpe := range v.URLs { if cpe != vv.URLs[idx] { return false } } for idx, item := range v.Cvss { if !reflect.DeepEqual(item, vv.Cvss[idx]) { return false } } return true } func NewMetadata(m *VulnerabilityMetadata) (*vulnerability.Metadata, error) { if m == nil { return nil, nil } return &vulnerability.Metadata{ ID: m.ID, DataSource: m.DataSource, Namespace: m.Namespace, Severity: m.Severity, URLs: m.URLs, Description: m.Description, Cvss: NewCvss(m.Cvss), }, nil } ================================================ FILE: grype/db/v5/vulnerability_metadata_store.go ================================================ package v5 type VulnerabilityMetadataStore interface { VulnerabilityMetadataStoreReader VulnerabilityMetadataStoreWriter } type VulnerabilityMetadataStoreReader interface { GetVulnerabilityMetadata(id, namespace string) (*VulnerabilityMetadata, error) GetAllVulnerabilityMetadata() (*[]VulnerabilityMetadata, error) } type VulnerabilityMetadataStoreWriter interface { AddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error } ================================================ FILE: grype/db/v5/vulnerability_store.go ================================================ package v5 const VulnerabilityStoreFileName = "vulnerability.db" type VulnerabilityStore interface { VulnerabilityStoreReader VulnerabilityStoreWriter } type VulnerabilityStoreReader interface { // GetVulnerabilityNamespaces retrieves unique list of vulnerability namespaces GetVulnerabilityNamespaces() ([]string, error) // GetVulnerability retrieves vulnerabilities by namespace and id GetVulnerability(namespace, id string) ([]Vulnerability, error) // SearchForVulnerabilities retrieves vulnerabilities by namespace and package SearchForVulnerabilities(namespace, packageName string) ([]Vulnerability, error) GetAllVulnerabilities() (*[]Vulnerability, error) } type VulnerabilityStoreWriter interface { // AddVulnerability inserts a new record of a vulnerability into the store AddVulnerability(vulnerabilities ...Vulnerability) error } ================================================ FILE: grype/db/v6/affected_cpe_store.go ================================================ package v6 import ( "gorm.io/gorm" "github.com/anchore/syft/syft/cpe" ) type AffectedCPEStoreWriter interface { AddAffectedCPEs(packages ...*AffectedCPEHandle) error } type AffectedCPEStoreReader interface { GetAffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]AffectedCPEHandle, error) } type affectedCPEStore struct { db *gorm.DB blobStore *blobStore cpeStore *cpeStore } func newAffectedCPEStore(db *gorm.DB, bs *blobStore) *affectedCPEStore { return &affectedCPEStore{ db: db, blobStore: bs, cpeStore: newCPEStore(db, bs), } } func (s *affectedCPEStore) AddAffectedCPEs(packages ...*AffectedCPEHandle) error { return addCPEHandles(s.cpeStore, packages...) } func (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]AffectedCPEHandle, error) { results, err := getCPEHandles[*AffectedCPEHandle]( s.cpeStore, cpe, config, "affected_cpe_handles", ) if err != nil { return nil, err } models := make([]AffectedCPEHandle, len(results)) for i, r := range results { models[i] = *r } return models, nil } ================================================ FILE: grype/db/v6/affected_cpe_store_test.go ================================================ package v6 import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" ) func TestAffectedCPEStore_AddAffectedCPEs(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newAffectedCPEStore(db, bw) cpe1 := &AffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ // vuln id = 1 Provider: &Provider{ ID: "nvd", }, Name: "CVE-2023-5678", }, CPE: &Cpe{ Part: "a", Vendor: "vendor-1", Product: "product-1", Edition: "edition-1", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } cpe2 := testAffectedCPEHandle() // vuln id = 2 err := s.AddAffectedCPEs(cpe1, cpe2) require.NoError(t, err) var result1 AffectedCPEHandle err = db.Where("cpe_id = ?", 1).First(&result1).Error require.NoError(t, err) assert.Equal(t, cpe1.VulnerabilityID, result1.VulnerabilityID) assert.Equal(t, cpe1.ID, result1.ID) assert.Equal(t, cpe1.BlobID, result1.BlobID) assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch var result2 AffectedCPEHandle err = db.Where("cpe_id = ?", 2).First(&result2).Error require.NoError(t, err) assert.Equal(t, cpe2.VulnerabilityID, result2.VulnerabilityID) assert.Equal(t, cpe2.ID, result2.ID) assert.Equal(t, cpe2.BlobID, result2.BlobID) assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch } func TestAffectedCPEStore_GetCPEs(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newAffectedCPEStore(db, bw) c := testAffectedCPEHandle() err := s.AddAffectedCPEs(c) require.NoError(t, err) results, err := s.GetAffectedCPEs(cpeFromProduct(c.CPE.Product), nil) require.NoError(t, err) expected := []AffectedCPEHandle{*c} require.Len(t, results, len(expected)) result := results[0] assert.Equal(t, c.CpeID, result.CpeID) assert.Equal(t, c.ID, result.ID) assert.Equal(t, c.BlobID, result.BlobID) require.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch // fetch again with blob & cpe preloaded results, err = s.GetAffectedCPEs(cpeFromProduct(c.CPE.Product), &GetCPEOptions{PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true}) require.NoError(t, err) require.Len(t, results, len(expected)) result = results[0] assert.NotNil(t, result.BlobValue) if d := cmp.Diff(*c, result); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } func TestAffectedCPEStore_GetExact(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newAffectedCPEStore(db, bw) c := testAffectedCPEHandle() err := s.AddAffectedCPEs(c) require.NoError(t, err) // we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses) results, err := s.GetAffectedCPEs(toCPE(c.CPE), nil) require.NoError(t, err) expected := []AffectedCPEHandle{*c} require.Len(t, results, len(expected)) result := results[0] assert.Equal(t, c.CpeID, result.CpeID) } func TestAffectedCPEStore_Get_CaseInsensitive(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newAffectedCPEStore(db, bw) c := testAffectedCPEHandle() err := s.AddAffectedCPEs(c) require.NoError(t, err) // we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses) results, err := s.GetAffectedCPEs(toCPE(&Cpe{ Part: "Application", // capitalized Vendor: "Vendor", // capitalized Product: "Product", // capitalized Edition: "Edition", // capitalized Language: "Language", // capitalized SoftwareEdition: "Software_edition", // capitalized TargetHardware: "Target_hardware", // capitalized TargetSoftware: "Target_software", // capitalized Other: "Other", // capitalized }), nil) require.NoError(t, err) expected := []AffectedCPEHandle{*c} require.Len(t, results, len(expected)) result := results[0] assert.Equal(t, c.CpeID, result.CpeID) } func TestAffectedCPEStore_PreventDuplicateCPEs(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newAffectedCPEStore(db, bw) cpe1 := &AffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ // vuln id = 1 Name: "CVE-2023-5678", Provider: &Provider{ ID: "nvd", }, }, CPE: &Cpe{ // ID = 1 Part: "a", Vendor: "vendor-1", Product: "product-1", Edition: "edition-1", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } err := s.AddAffectedCPEs(cpe1) require.NoError(t, err) // attempt to add a duplicate CPE with the same values duplicateCPE := &AffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ // vuln id = 2, different VulnerabilityID for testing... Name: "CVE-2024-1234", Provider: &Provider{ ID: "nvd", }, }, CpeID: 2, // for testing explicitly set to 2, but this is unrealistic CPE: &Cpe{ ID: 2, // different, again, unrealistic but useful for testing Part: "a", // same Vendor: "vendor-1", // same Product: "product-1", // same Edition: "edition-1", // same }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2024-1234"}, }, } err = s.AddAffectedCPEs(duplicateCPE) require.NoError(t, err) require.Equal(t, cpe1.CpeID, duplicateCPE.CpeID, "expected the CPE DB ID to be the same") var existingCPEs []Cpe err = db.Find(&existingCPEs).Error require.NoError(t, err) require.Len(t, existingCPEs, 1, "expected only one CPE to exist") actualHandles, err := s.GetAffectedCPEs(cpeFromProduct(cpe1.CPE.Product), &GetCPEOptions{ PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true, }) require.NoError(t, err) // the CPEs should be the same, and the store should reconcile the IDs duplicateCPE.CpeID = cpe1.CpeID duplicateCPE.CPE.ID = cpe1.CPE.ID expected := []AffectedCPEHandle{*cpe1, *duplicateCPE} require.Len(t, actualHandles, len(expected), "expected both handles to be stored") if d := cmp.Diff(expected, actualHandles); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } func cpeFromProduct(product string) *cpe.Attributes { return &cpe.Attributes{ Product: product, } } func toCPE(c *Cpe) *cpe.Attributes { return &cpe.Attributes{ Part: c.Part, Vendor: c.Vendor, Product: c.Product, Edition: c.Edition, Language: c.Language, SWEdition: c.SoftwareEdition, TargetSW: c.TargetSoftware, TargetHW: c.TargetHardware, Other: c.Other, } } func testAffectedCPEHandle() *AffectedCPEHandle { return &AffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2024-4321", Provider: &Provider{ ID: "nvd", }, }, CPE: &Cpe{ Part: "application", Vendor: "vendor", Product: "product", Edition: "edition", Language: "language", SoftwareEdition: "software_edition", TargetHardware: "target_hardware", TargetSoftware: "target_software", Other: "other", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2024-4321"}, }, } } ================================================ FILE: grype/db/v6/affected_package_store.go ================================================ package v6 import "gorm.io/gorm" type AffectedPackageStoreWriter interface { AddAffectedPackages(packages ...*AffectedPackageHandle) error } type AffectedPackageStoreReader interface { GetAffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]AffectedPackageHandle, error) } type affectedPackageStore struct { db *gorm.DB osStore *operatingSystemStore pkgStore *packageStore } func newAffectedPackageStore(db *gorm.DB, bs *blobStore, oss *operatingSystemStore) *affectedPackageStore { return &affectedPackageStore{ db: db, osStore: oss, pkgStore: newPackageStore(db, bs, oss), } } func (s *affectedPackageStore) AddAffectedPackages(packages ...*AffectedPackageHandle) error { return addPackagesWithOS(s.pkgStore, packages...) } func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]AffectedPackageHandle, error) { results, err := getPackages[*AffectedPackageHandle]( s.pkgStore, pkg, config, "affected_package_handles", ) if err != nil { return nil, err } models := make([]AffectedPackageHandle, len(results)) for i, r := range results { models[i] = *r } return models, nil } ================================================ FILE: grype/db/v6/affected_package_store_test.go ================================================ package v6 import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" ) type affectedPackageHandlePreloadConfig struct { name string PreloadOS bool PreloadPackage bool PreloadBlob bool PreloadVulnerability bool prepExpectations func(*testing.T, []AffectedPackageHandle) []AffectedPackageHandle } func defaultAffectedPackageHandlePreloadCases() []affectedPackageHandlePreloadConfig { return []affectedPackageHandlePreloadConfig{ { name: "preload-all", PreloadOS: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle { for _, a := range in { if a.OperatingSystemID != nil { require.NotNil(t, a.OperatingSystem) } require.NotNil(t, a.Package) require.NotNil(t, a.BlobValue) require.NotNil(t, a.Vulnerability) } return in }, }, { name: "preload-none", prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle { var out []AffectedPackageHandle for _, a := range in { if a.OperatingSystem == nil && a.BlobValue == nil && a.Package == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.Package = nil a.BlobValue = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-os-only", PreloadOS: true, prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle { var out []AffectedPackageHandle for _, a := range in { if a.OperatingSystemID != nil { require.NotNil(t, a.OperatingSystem) } if a.Package == nil && a.BlobValue == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.Package = nil a.BlobValue = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-package-only", PreloadPackage: true, prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle { var out []AffectedPackageHandle for _, a := range in { require.NotNil(t, a.Package) if a.OperatingSystem == nil && a.BlobValue == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.BlobValue = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-blob-only", PreloadBlob: true, prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle { var out []AffectedPackageHandle for _, a := range in { if a.OperatingSystem == nil && a.Package == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.Package = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-vulnerability-only", PreloadVulnerability: true, prepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle { var out []AffectedPackageHandle for _, a := range in { if a.OperatingSystem == nil && a.Package == nil && a.BlobValue == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.Package = nil a.BlobValue = nil out = append(out, a) } return out }, }, } } func TestAffectedPackageStore_AddAffectedPackages(t *testing.T) { setupAffectedPackageStore := func(t *testing.T) *affectedPackageStore { db := setupTestStore(t).db bs := newBlobStore(db) return newAffectedPackageStore(db, bs, newOperatingSystemStore(db, bs)) } setupTestStoreWithPackages := func(t *testing.T) (*AffectedPackageHandle, *AffectedPackageHandle, *affectedPackageStore) { pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1"}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := testDistro1AffectedPackage2Handle() return pkg1, pkg2, setupAffectedPackageStore(t) } t.Run("no preloading", func(t *testing.T) { pkg1, pkg2, s := setupTestStoreWithPackages(t) err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) var result1 AffectedPackageHandle err = s.db.Where("package_id = ?", pkg1.PackageID).First(&result1).Error require.NoError(t, err) assert.Equal(t, pkg1.PackageID, result1.PackageID) assert.Equal(t, pkg1.BlobID, result1.BlobID) require.Nil(t, result1.BlobValue) // no preloading on fetch var result2 AffectedPackageHandle err = s.db.Where("package_id = ?", pkg2.PackageID).First(&result2).Error require.NoError(t, err) assert.Equal(t, pkg2.PackageID, result2.PackageID) assert.Equal(t, pkg2.BlobID, result2.BlobID) require.Nil(t, result2.BlobValue) }) t.Run("preloading", func(t *testing.T) { pkg1, pkg2, s := setupTestStoreWithPackages(t) err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) options := &GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadBlob: true, } results, err := s.GetAffectedPackages(pkgFromName(pkg1.Package.Name), options) require.NoError(t, err) require.Len(t, results, 1) result := results[0] require.NotNil(t, result.Package) require.NotNil(t, result.BlobValue) assert.Nil(t, result.OperatingSystem) // pkg1 has no OS }) t.Run("preload CPEs", func(t *testing.T) { pkg1, _, s := setupTestStoreWithPackages(t) c := Cpe{ Part: "a", Vendor: "vendor1", Product: "product1", } pkg1.Package.CPEs = []Cpe{c} err := s.AddAffectedPackages(pkg1) require.NoError(t, err) options := &GetPackageOptions{ PreloadPackage: true, PreloadPackageCPEs: true, } results, err := s.GetAffectedPackages(pkgFromName(pkg1.Package.Name), options) require.NoError(t, err) require.Len(t, results, 1) result := results[0] require.NotNil(t, result.Package) // the IDs should have been set, and there is only one, so we know the correct values c.ID = 1 if d := cmp.Diff([]Cpe{c}, result.Package.CPEs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) t.Run("Package deduplication", func(t *testing.T) { pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1"}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1"}, // same! BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-56789"}, }, } s := setupAffectedPackageStore(t) err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) var pkgs []Package err = s.db.Find(&pkgs).Error require.NoError(t, err) expected := []Package{ *pkg1.Package, } if d := cmp.Diff(expected, pkgs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) t.Run("same package with multiple CPEs", func(t *testing.T) { cpe1 := Cpe{ Part: "a", Vendor: "vendor1", Product: "product1", } cpe2 := Cpe{ Part: "a", Vendor: "vendor2", Product: "product2", } pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-56789", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1, cpe2}}, // duplicate CPE + additional CPE BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-56789"}, }, } s := setupAffectedPackageStore(t) err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) var pkgs []Package err = s.db.Preload("CPEs").Find(&pkgs).Error require.NoError(t, err) expPkg := *pkg1.Package expPkg.ID = 1 cpe1.ID = 1 cpe2.ID = 2 expPkg.CPEs = []Cpe{cpe1, cpe2} expected := []Package{ expPkg, } if d := cmp.Diff(expected, pkgs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } expectedCPEs := []Cpe{cpe1, cpe2} var cpeResults []Cpe err = s.db.Find(&cpeResults).Error require.NoError(t, err) if d := cmp.Diff(expectedCPEs, cpeResults); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) t.Run("allow same CPE to belong to multiple packages", func(t *testing.T) { cpe1 := Cpe{ Part: "a", Vendor: "vendor1", Product: "product1", } cpe2 := Cpe{ Part: "a", Vendor: "vendor2", Product: "product2", } pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-56789", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg2", Ecosystem: "type1", CPEs: []Cpe{cpe1, cpe2}}, // overlapping CPEs for different packages BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-56789"}, }, } s := setupAffectedPackageStore(t) err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) var pkgs []Package err = s.db.Preload("CPEs").Find(&pkgs).Error require.NoError(t, err) cpe1.ID = 1 cpe2.ID = 2 expPkg1 := *pkg1.Package expPkg1.ID = 1 expPkg1.CPEs = []Cpe{cpe1} expPkg2 := *pkg2.Package expPkg2.ID = 2 expPkg2.CPEs = []Cpe{cpe1, cpe2} expected := []Package{ expPkg1, expPkg2, } if d := cmp.Diff(expected, pkgs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } expectedCPEs := []Cpe{cpe1, cpe2} var cpeResults []Cpe err = s.db.Find(&cpeResults).Error require.NoError(t, err) if d := cmp.Diff(expectedCPEs, cpeResults); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) } func TestAffectedPackageStore_GetAffectedPackages_ByCPE(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newAffectedPackageStore(db, bs, oss) cpe1 := Cpe{Part: "a", Vendor: "vendor1", Product: "product1"} cpe2 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2"} cpe3 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2", TargetSoftware: "target1"} pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-5678", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg2", Ecosystem: "type2", CPEs: []Cpe{cpe2}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } pkg3 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-5678", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg3", Ecosystem: "type2", CPEs: []Cpe{cpe3}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } err := s.AddAffectedPackages(pkg1, pkg2, pkg3) require.NoError(t, err) tests := []struct { name string cpe cpe.Attributes options *GetPackageOptions expected []AffectedPackageHandle wantErr require.ErrorAssertionFunc }{ { name: "full match CPE", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor1", Product: "product1", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: []AffectedPackageHandle{*pkg1}, }, { name: "partial match CPE", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor2", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: []AffectedPackageHandle{*pkg2, *pkg3}, }, { name: "match on any TSW when specific one provided when broad matching enabled", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor2", TargetSW: "target1", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, AllowBroadCPEMatching: true, }, expected: []AffectedPackageHandle{*pkg2, *pkg3}, }, { name: "do NOT match on any TSW when specific one provided when broad matching disabled", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor2", TargetSW: "target1", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, AllowBroadCPEMatching: false, }, expected: []AffectedPackageHandle{*pkg3}, }, { name: "missing attributes", cpe: cpe.Attributes{ Part: "a", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: []AffectedPackageHandle{*pkg1, *pkg2, *pkg3}, }, { name: "no matches", cpe: cpe.Attributes{ Part: "a", Vendor: "unknown_vendor", Product: "unknown_product", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } result, err := s.GetAffectedPackages(&PackageSpecifier{CPE: &tt.cpe}, tt.options) tt.wantErr(t, err) if err != nil { return } if d := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); d != "" { t.Errorf("unexpected result: %s", d) } }) } } func TestAffectedPackageStore_GetAffectedPackages_CaseInsensitive(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newAffectedPackageStore(db, bs, oss) cpe1 := Cpe{Part: "a", Vendor: "Vendor1", Product: "Product1"} // capitalized pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, OperatingSystem: &OperatingSystem{ Name: "Ubuntu", // capitalized ReleaseID: "zubuntu", MajorVersion: "20", MinorVersion: "04", // leading 0 Codename: "focal", }, Package: &Package{Name: "Pkg1", Ecosystem: "Type1", CPEs: []Cpe{cpe1}}, // capitalized BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &AffectedPackageHandle{ // this should never register as a match Vulnerability: &VulnerabilityHandle{ Name: "CVE-2222-2222", Provider: &Provider{ ID: "provider2", }, }, OperatingSystem: &OperatingSystem{ Name: "ubuntu", ReleaseID: "ubuntu", MajorVersion: "20", MinorVersion: "10", }, Package: &Package{Name: "pkg2", Ecosystem: "type2"}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2222-2222"}, }, } err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) tests := []struct { name string pkgSpec *PackageSpecifier options *GetPackageOptions expected int }{ { name: "sanity check: search miss", pkgSpec: pkgFromName("does not exist"), expected: 0, }, { name: "get by name", pkgSpec: pkgFromName("pKG1"), expected: 1, }, { name: "get by CPE", pkgSpec: &PackageSpecifier{ CPE: &cpe.Attributes{Part: "a", Vendor: "veNDor1", Product: "pRODuct1"}, }, expected: 1, }, { name: "get by ecosystem", pkgSpec: &PackageSpecifier{ Ecosystem: "tYPE1", }, expected: 1, }, { name: "get by OS name and version (leading 0)", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "uBUNtu", MajorVersion: "20", MinorVersion: "04", }}, }, expected: 1, }, { name: "get by OS name and version", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "uBUNtu", MajorVersion: "20", MinorVersion: "4", }}, }, expected: 1, }, { name: "get by OS release", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "zUBuntu", }}, }, expected: 1, }, { name: "get by OS codename", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ LabelVersion: "fOCAL", }}, }, expected: 1, }, { name: "get by vuln ID", options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{Name: "cVe-2023-1234"}}, }, expected: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := s.GetAffectedPackages(tt.pkgSpec, tt.options) require.NoError(t, err) require.Len(t, result, tt.expected) if tt.expected > 0 { assert.Equal(t, pkg1.PackageID, result[0].PackageID) } }) } } func TestAffectedPackageStore_GetAffectedPackages_MultipleVulnerabilitySpecs(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newAffectedPackageStore(db, bs, oss) cpe1 := Cpe{Part: "a", Vendor: "vendor1", Product: "product1"} cpe2 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2"} pkg1 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &AffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-5678", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg2", Ecosystem: "type2", CPEs: []Cpe{cpe2}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } err := s.AddAffectedPackages(pkg1, pkg2) require.NoError(t, err) result, err := s.GetAffectedPackages(nil, &GetPackageOptions{ PreloadVulnerability: true, Vulnerabilities: []VulnerabilitySpecifier{ {Name: "CVE-2023-1234"}, {Name: "CVE-2023-5678"}, }, }) require.NoError(t, err) actualVulns := strset.New() for _, r := range result { actualVulns.Add(r.Vulnerability.Name) } expectedVulns := strset.New("CVE-2023-1234", "CVE-2023-5678") assert.ElementsMatch(t, expectedVulns.List(), actualVulns.List()) } func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newAffectedPackageStore(db, bs, oss) pkg2d1 := testDistro1AffectedPackage2Handle() pkg2 := testNonDistroAffectedPackage2Handle() pkg2d2 := testDistro2AffectedPackage2Handle() err := s.AddAffectedPackages(pkg2d1, pkg2, pkg2d2) require.NoError(t, err) tests := []struct { name string pkg *PackageSpecifier options *GetPackageOptions expected []AffectedPackageHandle wantErr require.ErrorAssertionFunc }{ { name: "specific distro", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, { name: "distro major version only", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", }}, }, expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2}, }, { name: "distro codename", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "ubuntu", LabelVersion: "groovy", }}, }, expected: []AffectedPackageHandle{*pkg2d2}, }, { name: "no distro", pkg: pkgFromName(pkg2.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{NoOSSpecified}, }, expected: []AffectedPackageHandle{*pkg2}, }, { name: "any distro", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{AnyOSSpecified}, }, expected: []AffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2}, }, { name: "package type", pkg: &PackageSpecifier{Name: pkg2.Package.Name, Ecosystem: "type2"}, expected: []AffectedPackageHandle{*pkg2}, }, { name: "specific CVE", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ Name: "CVE-2023-1234", }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, { name: "any CVE published after a date", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ PublishedAfter: func() *time.Time { now := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC) return &now }(), }}, }, expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2}, }, { name: "any CVE modified after a date", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ ModifiedAfter: func() *time.Time { now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC).Add(time.Hour * 2) return &now }(), }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, { name: "any rejected CVE", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ Status: VulnerabilityRejected, }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } for _, pc := range defaultAffectedPackageHandlePreloadCases() { t.Run(pc.name, func(t *testing.T) { opts := tt.options if opts == nil { opts = &GetPackageOptions{} } opts.PreloadOS = pc.PreloadOS opts.PreloadPackage = pc.PreloadPackage opts.PreloadBlob = pc.PreloadBlob opts.PreloadVulnerability = pc.PreloadVulnerability expected := tt.expected if pc.prepExpectations != nil { expected = pc.prepExpectations(t, expected) } result, err := s.GetAffectedPackages(tt.pkg, opts) tt.wantErr(t, err) if err != nil { return } if d := cmp.Diff(expected, result); d != "" { t.Errorf("unexpected result: %s", d) } }) } }) } } func TestAffectedPackageStore_ApplyPackageAlias(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newAffectedPackageStore(db, bs, oss) tests := []struct { name string input *PackageSpecifier expected string }{ // positive cases {name: "alias cocoapods", input: &PackageSpecifier{Ecosystem: "cocoapods"}, expected: "pod"}, {name: "alias pub", input: &PackageSpecifier{Ecosystem: "pub"}, expected: "dart-pub"}, {name: "alias otp", input: &PackageSpecifier{Ecosystem: "otp"}, expected: "erlang-otp"}, {name: "alias github", input: &PackageSpecifier{Ecosystem: "github"}, expected: "github-action"}, {name: "alias golang", input: &PackageSpecifier{Ecosystem: "golang"}, expected: "go-module"}, {name: "alias maven", input: &PackageSpecifier{Ecosystem: "maven"}, expected: "java-archive"}, {name: "alias composer", input: &PackageSpecifier{Ecosystem: "composer"}, expected: "php-composer"}, {name: "alias pecl", input: &PackageSpecifier{Ecosystem: "pecl"}, expected: "php-pecl"}, {name: "alias pypi", input: &PackageSpecifier{Ecosystem: "pypi"}, expected: "python"}, {name: "alias cran", input: &PackageSpecifier{Ecosystem: "cran"}, expected: "R-package"}, {name: "alias luarocks", input: &PackageSpecifier{Ecosystem: "luarocks"}, expected: "lua-rocks"}, {name: "alias cargo", input: &PackageSpecifier{Ecosystem: "cargo"}, expected: "rust-crate"}, // negative cases {name: "generic type", input: &PackageSpecifier{Ecosystem: "generic/linux-kernel"}, expected: "generic/linux-kernel"}, {name: "empty ecosystem", input: &PackageSpecifier{Ecosystem: ""}, expected: ""}, {name: "matching type", input: &PackageSpecifier{Ecosystem: "python"}, expected: "python"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := s.pkgStore.applyPackageAlias(tt.input) require.NoError(t, err) assert.Equal(t, tt.expected, tt.input.Ecosystem) }) } } func testDistro1AffectedPackage2Handle() *AffectedPackageHandle { now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC) later := now.Add(time.Hour * 200) return &AffectedPackageHandle{ Package: &Package{ Name: "pkg2", Ecosystem: "type2d", }, Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Status: VulnerabilityRejected, PublishedDate: &now, ModifiedDate: &later, Provider: &Provider{ ID: "ubuntu", }, }, OperatingSystem: &OperatingSystem{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } } func testDistro2AffectedPackage2Handle() *AffectedPackageHandle { now := time.Date(2020, 1, 1, 3, 4, 5, 0, time.UTC) later := now.Add(time.Hour * 200) return &AffectedPackageHandle{ Package: &Package{ Name: "pkg2", Ecosystem: "type2d", }, Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-4567", PublishedDate: &now, ModifiedDate: &later, Provider: &Provider{ ID: "ubuntu", }, }, OperatingSystem: &OperatingSystem{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "10", LabelVersion: "groovy", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-4567"}, }, } } func testNonDistroAffectedPackage2Handle() *AffectedPackageHandle { now := time.Date(2005, 1, 1, 3, 4, 5, 0, time.UTC) later := now.Add(time.Hour * 200) return &AffectedPackageHandle{ Package: &Package{ Name: "pkg2", Ecosystem: "type2", }, Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-4567", PublishedDate: &now, ModifiedDate: &later, Provider: &Provider{ ID: "wolfi", }, }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-4567"}, }, } } func expectErrIs(t *testing.T, expected error) require.ErrorAssertionFunc { t.Helper() return func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.Error(t, err, msgAndArgs...) assert.ErrorIs(t, err, expected) } } func pkgFromName(name string) *PackageSpecifier { return &PackageSpecifier{Name: name} } ================================================ FILE: grype/db/v6/blob_store.go ================================================ package v6 import ( "encoding/json" "fmt" "strings" "time" "gorm.io/gorm" "github.com/anchore/grype/internal/log" ) type blobable interface { getBlobID() ID getBlobValue() any setBlobID(ID) setBlob([]byte) error } type blobStore struct { db *gorm.DB idsByDigest map[string]ID } func newBlobStore(db *gorm.DB) *blobStore { return &blobStore{ db: db, idsByDigest: make(map[string]ID), } } func (s *blobStore) addBlobable(bs ...blobable) error { for i := range bs { b := bs[i] v := b.getBlobValue() if v == nil { continue } bl := newBlob(v) if err := s.addBlobs(bl); err != nil { return err } b.setBlobID(bl.ID) } return nil } func (s *blobStore) addBlobs(blobs ...*Blob) error { for i := range blobs { v := blobs[i] digest := v.computeDigest() if id, ok := s.idsByDigest[digest]; ok && id != 0 { v.ID = id continue } if err := s.db.Create(v).Error; err != nil { return fmt.Errorf("failed to create blob: %w", err) } if v.ID != 0 { s.idsByDigest[digest] = v.ID } } return nil } func (s *blobStore) getBlobValues(ids ...ID) ([]Blob, error) { if len(ids) == 0 { return nil, nil } var blobs []Blob if err := s.db.Where("id IN ?", ids).Find(&blobs).Error; err != nil { return nil, fmt.Errorf("failed to get blob values: %w", err) } return blobs, nil } func (s *blobStore) attachBlobValue(bs ...blobable) error { start := time.Now() defer func() { log.WithFields("duration", time.Since(start), "count", len(bs)).Trace("attached blob values") }() var ids []ID var setterByID = make(map[ID][]blobable) for i := range bs { b := bs[i] id := b.getBlobID() // skip fetching this blob if there is no blobID, or if we already have this blob if id == 0 || b.getBlobValue() != nil { continue } ids = append(ids, id) setterByID[id] = append(setterByID[id], b) } vs, err := s.getBlobValues(ids...) if err != nil { return fmt.Errorf("failed to get blob value: %w", err) } for _, b := range vs { if b.Value == "" { continue } for _, setter := range setterByID[b.ID] { if err := setter.setBlob([]byte(b.Value)); err != nil { return fmt.Errorf("failed to set blob value: %w", err) } } } return nil } func newBlob(obj any) *Blob { sb := strings.Builder{} enc := json.NewEncoder(&sb) enc.SetEscapeHTML(false) if err := enc.Encode(obj); err != nil { panic("could not marshal object to json") } return &Blob{ Value: sb.String(), } } ================================================ FILE: grype/db/v6/blob_store_test.go ================================================ package v6 import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBlobWriter_AddBlobs(t *testing.T) { db := setupTestStore(t).db writer := newBlobStore(db) obj1 := map[string]string{"key": "value1"} obj2 := map[string]string{"key": "value2"} blob1 := newBlob(obj1) blob2 := newBlob(obj2) blob3 := newBlob(obj1) // same as blob1 err := writer.addBlobs(blob1, blob2, blob3) require.NoError(t, err) require.NotZero(t, blob1.ID) require.Equal(t, blob1.ID, blob3.ID) // blob3 should have the same ID as blob1 (natural deduplication) var result1 Blob require.NoError(t, db.Where("id = ?", blob1.ID).First(&result1).Error) assert.Equal(t, blob1.Value, result1.Value) var result2 Blob require.NoError(t, db.Where("id = ?", blob2.ID).First(&result2).Error) assert.Equal(t, blob2.Value, result2.Value) } func TestBlob_computeDigest(t *testing.T) { assert.Equal(t, "xxh64:0e6882304e9adbd5", Blob{Value: "test content"}.computeDigest()) assert.Equal(t, "xxh64:ea0c19ae9fbd93b3", Blob{Value: "different content"}.computeDigest()) } ================================================ FILE: grype/db/v6/blobs.go ================================================ package v6 import ( "encoding/json" "fmt" "strings" "time" ) // VulnerabilityBlob represents the core advisory record for a single known vulnerability from a specific provider. type VulnerabilityBlob struct { // ID is the lowercase unique string identifier for the vulnerability relative to the provider ID string `json:"id"` // Assigners is a list of names, email, or organizations who submitted the vulnerability Assigners []string `json:"assigner,omitempty"` // Description of the vulnerability as provided by the source Description string `json:"description,omitempty"` // References are URLs to external resources that provide more information about the vulnerability References []Reference `json:"refs,omitempty"` // Aliases is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases. Aliases []string `json:"aliases,omitempty"` // Severities is a list of severity indications (quantitative or qualitative) for the vulnerability Severities []Severity `json:"severities,omitempty"` } func (v VulnerabilityBlob) String() string { return v.ID } // Reference represents a single external URL and string tags to use for organizational purposes type Reference struct { // URL is the external resource URL string `json:"url"` // ID is an optional identifier for the reference (e.g., advisory ID like "RHSA-2023:5455") ID string `json:"id,omitempty"` // Tags is a free-form organizational field to convey additional information about the reference Tags []string `json:"tags,omitempty"` } // Severity represents a single string severity record for a vulnerability record type Severity struct { // Scheme describes the quantitative method used to determine the Score, such as "CVSS_V3". Alternatively this makes // claim that Value is qualitative, for example "HML" (High, Medium, Low), CHMLN (critical-high-medium-low-negligible) Scheme SeverityScheme `json:"scheme"` // Value is the severity score (e.g. "7.5", "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N", or "high" ) Value any `json:"value"` // one of CVSSSeverity, HMLSeverity, CHMLNSeverity // Source is the name of the source of the severity score (e.g. "nvd@nist.gov" or "security-advisories@github.com") Source string `json:"source,omitempty"` // Rank is a free-form organizational field to convey priority over other severities Rank int `json:"rank"` } type severityAlias Severity type severityUnmarshalProxy struct { *severityAlias Value json.RawMessage `json:"value"` } // UnmarshalJSON custom unmarshaller for Severity struct func (s *Severity) UnmarshalJSON(data []byte) error { aux := &severityUnmarshalProxy{ severityAlias: (*severityAlias)(s), } if err := json.Unmarshal(data, aux); err != nil { return err } var cvss CVSSSeverity if err := json.Unmarshal(aux.Value, &cvss); err == nil && cvss.Vector != "" { s.Value = cvss return nil } var strSeverity string if err := json.Unmarshal(aux.Value, &strSeverity); err == nil { s.Value = strSeverity return nil } return fmt.Errorf("could not unmarshal severity value to known type: %s", aux.Value) } // CVSSSeverity represents a single Common Vulnerability Scoring System entry type CVSSSeverity struct { // Vector is the CVSS assessment as a parameterized string Vector string `json:"vector"` // Version is the CVSS version (e.g. "3.0") Version string `json:"version,omitempty"` } func (c CVSSSeverity) String() string { vector := c.Vector if !strings.HasPrefix(strings.ToLower(c.Vector), "cvss:") && c.Version != "" { vector = fmt.Sprintf("CVSS:%s/%s", c.Version, c.Vector) } return vector } // PackageBlob represents a package that is affected by a vulnerability. type PackageBlob struct { // CVEs is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability. CVEs []string `json:"cves,omitempty"` // Qualifiers are package attributes that confirm the package is affected by the vulnerability. Qualifiers *PackageQualifiers `json:"qualifiers,omitempty"` // Ranges specifies the affected version ranges and fixes if available. Ranges []Range `json:"ranges,omitempty"` } func (a PackageBlob) String() string { var fields []string if len(a.Ranges) > 0 { var ranges []string for _, r := range a.Ranges { ranges = append(ranges, r.String()) } fields = append(fields, fmt.Sprintf("ranges=%s", strings.Join(ranges, ", "))) } if len(a.CVEs) > 0 { fields = append(fields, fmt.Sprintf("cves=%s", strings.Join(a.CVEs, ", "))) } return strings.Join(fields, ", ") } // PackageQualifiers contains package attributes that should hold true to associate a vulnerablity to that package. type PackageQualifiers struct { // RpmModularity indicates if the package follows RPM modularity for versioning. RpmModularity *string `json:"rpm_modularity,omitempty"` // PlatformCPEs lists Common Platform Enumeration (CPE) identifiers for affected platforms. PlatformCPEs []string `json:"platform_cpes,omitempty"` } // Range defines a specific range of package versions pertaining to a vulnerability. type Range struct { // Version defines the version constraints for affected software. Version Version `json:"version,omitempty"` // Fix provides details on the fix version and its state if available. Fix *Fix `json:"fix,omitempty"` } func (a Range) String() string { return fmt.Sprintf("%s (%s)", a.Version, a.Fix) } // Fix conveys availability of a fix for a vulnerability. type Fix struct { // Version is the version number of the fix. Version string `json:"version,omitempty"` // State represents the status of the fix (e.g., "fixed", "unaffected"). State FixStatus `json:"state,omitempty"` // Detail provides additional fix information, such as commit details. Detail *FixDetail `json:"detail,omitempty"` } func (f Fix) String() string { switch f.State { case FixedStatus: return fmt.Sprintf("fixed in %s", f.Version) case NotAffectedFixStatus: return fmt.Sprintf("%s is not affected", f.Version) } return string(f.State) } // FixDetail is additional information about a fix, such as commit details and patch URLs. type FixDetail struct { // Available indicates when the fix information became available and how it was obtained. Available *FixAvailability `json:"available,omitempty"` // References contains URLs or identifiers for additional resources on the fix. References []Reference `json:"references,omitempty"` } type FixAvailability struct { // Date is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged. Date *time.Time `json:"date,omitempty"` // Kind describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record) Kind string `json:"kind,omitempty"` } func (f FixAvailability) MarshalJSON() ([]byte, error) { type Alias FixAvailability aux := &struct { Date *string `json:"date,omitempty"` *Alias }{ Alias: (*Alias)(&f), } // the JSON marshaller should interpret the time.Time as a Date, not a timestamp if f.Date != nil { dateStr := f.Date.Format("2006-01-02") aux.Date = &dateStr } return json.Marshal(aux) } func (f *FixAvailability) UnmarshalJSON(data []byte) error { type Alias FixAvailability aux := &struct { Date *string `json:"date,omitempty"` *Alias }{ Alias: (*Alias)(f), } if err := json.Unmarshal(data, aux); err != nil { return err } if aux.Date != nil { if t, err := time.Parse("2006-01-02", *aux.Date); err == nil { f.Date = &t return nil } if t, err := time.Parse(time.RFC3339, *aux.Date); err == nil { f.Date = &t return nil } return fmt.Errorf("unable to parse date %q: expected format YYYY-MM-DD or RFC3339", *aux.Date) } return nil } // Version defines the versioning format and constraints. type Version struct { // Type specifies the versioning system used (e.g., "semver", "rpm"). Type string `json:"type,omitempty"` // Constraint defines the version range constraint for affected versions. Constraint string `json:"constraint,omitempty"` } type KnownExploitedVulnerabilityBlob struct { Cve string `json:"cve"` VendorProject string `json:"vendor_project,omitempty"` Product string `json:"product,omitempty"` DateAdded *time.Time `json:"date_added,omitempty"` RequiredAction string `json:"required_action,omitempty"` DueDate *time.Time `json:"due_date,omitempty"` KnownRansomwareCampaignUse string `json:"known_ransomware_campaign_use,omitempty"` Notes string `json:"notes,omitempty"` URLs []string `json:"urls,omitempty"` CWEs []string `json:"cwes,omitempty"` } ================================================ FILE: grype/db/v6/blobs_test.go ================================================ package v6 import ( "encoding/json" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFixAvailability_MarshalJSON(t *testing.T) { testTime := time.Date(2022, 4, 9, 15, 30, 45, 0, time.UTC) fixAvail := FixAvailability{ Date: &testTime, Kind: "advisory", } jsonData, err := json.Marshal(fixAvail) require.NoError(t, err) expected := `{"date":"2022-04-09","kind":"advisory"}` assert.JSONEq(t, expected, string(jsonData)) } func TestFixAvailability_UnmarshalJSON_SimpleDateFormat(t *testing.T) { jsonData := `{"date":"2022-04-09","kind":"advisory"}` var fixAvail FixAvailability err := json.Unmarshal([]byte(jsonData), &fixAvail) require.NoError(t, err) expectedTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC) assert.Equal(t, &expectedTime, fixAvail.Date) assert.Equal(t, "advisory", fixAvail.Kind) } func TestFixAvailability_UnmarshalJSON_RFC3339Format(t *testing.T) { jsonData := `{"date":"2022-04-09T00:00:00Z","kind":"advisory"}` var fixAvail FixAvailability err := json.Unmarshal([]byte(jsonData), &fixAvail) require.NoError(t, err) expectedTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC) assert.Equal(t, &expectedTime, fixAvail.Date) assert.Equal(t, "advisory", fixAvail.Kind) } func TestFixAvailability_RoundTripMarshalUnmarshal(t *testing.T) { originalTime := time.Date(2022, 4, 9, 15, 30, 45, 0, time.UTC) original := FixAvailability{ Date: &originalTime, Kind: "advisory", } jsonData, err := json.Marshal(original) require.NoError(t, err) var unmarshaled FixAvailability err = json.Unmarshal(jsonData, &unmarshaled) require.NoError(t, err) // Time precision is lost during marshaling - only date is preserved expectedTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC) assert.Equal(t, &expectedTime, unmarshaled.Date) assert.Equal(t, "advisory", unmarshaled.Kind) } func TestPackageBlob_WithFixAvailability(t *testing.T) { testTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC) blob := PackageBlob{ CVEs: []string{"CVE-2021-3521"}, Ranges: []Range{ { Version: Version{ Type: "rpm", Constraint: "< 0:4.14.2-15.cm1", }, Fix: &Fix{ Version: "0:4.14.2-15.cm1", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: &testTime, Kind: "advisory", }, }, }, }, }, } jsonData, err := json.Marshal(blob) require.NoError(t, err) assert.Contains(t, string(jsonData), `"date":"2022-04-09"`) assert.NotContains(t, string(jsonData), `"date":"2022-04-09T`) var unmarshaledBlob PackageBlob err = json.Unmarshal(jsonData, &unmarshaledBlob) require.NoError(t, err) require.Len(t, unmarshaledBlob.Ranges, 1) require.NotNil(t, unmarshaledBlob.Ranges[0].Fix) require.NotNil(t, unmarshaledBlob.Ranges[0].Fix.Detail) require.NotNil(t, unmarshaledBlob.Ranges[0].Fix.Detail.Available) assert.Equal(t, &testTime, unmarshaledBlob.Ranges[0].Fix.Detail.Available.Date) assert.Equal(t, "advisory", unmarshaledBlob.Ranges[0].Fix.Detail.Available.Kind) } func TestFixAvailability_UnmarshalJSON_InvalidDateFormat(t *testing.T) { jsonData := `{"date":"invalid-date","kind":"advisory"}` var fixAvail FixAvailability err := json.Unmarshal([]byte(jsonData), &fixAvail) require.Error(t, err) assert.Contains(t, err.Error(), `unable to parse date "invalid-date"`) assert.Contains(t, err.Error(), "expected format YYYY-MM-DD or RFC3339") } ================================================ FILE: grype/db/v6/build/archive.go ================================================ package v6 import ( "errors" "fmt" "os" "path" "path/filepath" "strings" "time" "github.com/anchore/grype/grype/db/internal/tarutil" "github.com/anchore/grype/grype/db/provider" v6 "github.com/anchore/grype/grype/db/v6" v6Distribution "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/internal/log" ) func CreateArchive(dbDir, overrideArchiveExtension string, compressorCommands map[string]string) error { extension, err := resolveExtension(overrideArchiveExtension) if err != nil { return err } log.WithFields("from", dbDir, "extension", extension).Info("packaging database") cfg := v6.Config{DBDirPath: dbDir} r, err := v6.NewReader(cfg) if err != nil { return fmt.Errorf("unable to open vulnerability store: %w", err) } metadata, err := r.GetDBMetadata() if err != nil || metadata == nil { return fmt.Errorf("unable to get vulnerability store metadata: %w", err) } if metadata.Model != v6.ModelVersion { return fmt.Errorf("metadata model %d does not match vulnerability store model %d", v6.ModelVersion, metadata.Model) } providerModels, err := r.AllProviders() if err != nil { return fmt.Errorf("unable to get all providers: %w", err) } if len(providerModels) == 0 { return fmt.Errorf("no providers found in the vulnerability store") } eldest, err := toProviders(providerModels).EarliestTimestamp() if err != nil { return err } // output archive vulnerability-db_VERSION_OLDESTDATADATE_BUILTEPOCH.tar.gz, where: // - VERSION: schema version in the form of v#.#.# // - OLDESTDATADATE: RFC3339 formatted value (e.g. 2020-06-18T17:24:53Z) of the oldest date capture date found for all contained providers // - BUILTEPOCH: linux epoch formatted value of the database metadata built field tarName := fmt.Sprintf( "vulnerability-db_v%s_%s_%d.%s", fmt.Sprintf("%d.%d.%d", metadata.Model, metadata.Revision, metadata.Addition), eldest.UTC().Format(time.RFC3339), metadata.BuildTimestamp.Unix(), extension, ) tarPath := filepath.Join(dbDir, tarName) files := []string{v6.VulnerabilityDBFileName} if _, err := os.Stat(path.Join(dbDir, v6.ImportMetadataFileName)); err == nil { files = append(files, v6.ImportMetadataFileName) } if err := populateTar(dbDir, tarName, compressorCommands, files...); err != nil { return err } log.WithFields("path", tarPath).Info("created database archive") return writeLatestDocument(tarPath, *metadata) } func toProviders(states []v6.Provider) provider.States { var result provider.States for _, state := range states { result = append(result, provider.State{ Provider: state.ID, Timestamp: *state.DateCaptured, }) } return result } func resolveExtension(overrideArchiveExtension string) (string, error) { var extension = "tar.zst" if overrideArchiveExtension != "" { extension = strings.TrimLeft(overrideArchiveExtension, ".") } var found bool for _, valid := range []string{"tar.zst", "tar.xz", "tar.gz"} { if valid == extension { found = true break } } if !found { return "", fmt.Errorf("unsupported archive extension %q", extension) } return extension, nil } func populateTar(dbDir, tarName string, compressorCommands map[string]string, files ...string) error { originalDir, err := os.Getwd() if err != nil { return fmt.Errorf("unable to get CWD: %w", err) } if dbDir != "" { if err = os.Chdir(dbDir); err != nil { return fmt.Errorf("unable to cd to build dir: %w", err) } defer func() { if err = os.Chdir(originalDir); err != nil { log.Errorf("unable to cd to original dir: %v", err) } }() } for _, f := range files { _, err := os.Stat(f) if err != nil { return fmt.Errorf("unable to stat file %q: %w", f, err) } } if err = tarutil.PopulateWithPathsAndCompressors(tarName, compressorCommands, files...); err != nil { return fmt.Errorf("unable to create db archive: %w", err) } return nil } func writeLatestDocument(tarPath string, metadata v6.DBMetadata) error { archive, err := v6Distribution.NewArchive(tarPath, *metadata.BuildTimestamp, metadata.Model, metadata.Revision, metadata.Addition) if err != nil || archive == nil { return fmt.Errorf("unable to create archive: %w", err) } doc := v6Distribution.NewLatestDocument(*archive) if doc == nil { return errors.New("unable to create latest document") } dbDir := filepath.Dir(tarPath) latestPath := path.Join(dbDir, v6Distribution.LatestFileName) fh, err := os.OpenFile(latestPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("unable to create latest file: %w", err) } if err = doc.Write(fh); err != nil { return fmt.Errorf("unable to write latest document: %w", err) } return nil } ================================================ FILE: grype/db/v6/build/processors.go ================================================ package v6 import ( "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/processors" "github.com/anchore/grype/grype/db/v6/build/transformers/eol" "github.com/anchore/grype/grype/db/v6/build/transformers/epss" "github.com/anchore/grype/grype/db/v6/build/transformers/github" "github.com/anchore/grype/grype/db/v6/build/transformers/kev" "github.com/anchore/grype/grype/db/v6/build/transformers/msrc" "github.com/anchore/grype/grype/db/v6/build/transformers/nvd" "github.com/anchore/grype/grype/db/v6/build/transformers/openvex" "github.com/anchore/grype/grype/db/v6/build/transformers/os" "github.com/anchore/grype/grype/db/v6/build/transformers/osv" ) type Config struct { NVD nvd.Config } type Option func(cfg *Config) func WithCPEParts(included []string) Option { return func(cfg *Config) { cfg.NVD.CPEParts = strset.New(included...) } } func WithInferNVDFixVersions(infer bool) Option { return func(cfg *Config) { cfg.NVD.InferNVDFixVersions = infer } } func NewConfig(options ...Option) Config { var cfg Config for _, option := range options { option(&cfg) } return cfg } func Processors(cfg Config) []data.Processor { return []data.Processor{ processors.NewV2GitHubProcessor(github.Transform), processors.NewV2MSRCProcessor(msrc.Transform), processors.NewV2NVDProcessor(nvd.Transformer(cfg.NVD)), processors.NewV2OSProcessor(os.Transform), processors.NewV2OSVProcessor(osv.Transform), processors.NewV2KEVProcessor(kev.Transform), processors.NewV2EPSSProcessor(epss.Transform), processors.NewV2OpenVEXProcessor(openvex.Transform), processors.NewV2AnnotatedOpenVEXProcessor(openvex.AnnotatedTransform), // EOL processor must be last to update existing OS records processors.NewV2EOLProcessor(eol.Transform), } } ================================================ FILE: grype/db/v6/build/transformers/entry.go ================================================ package transformers import ( "fmt" "strings" "github.com/anchore/grype/grype/db/data" db "github.com/anchore/grype/grype/db/v6" ) type RelatedEntries struct { VulnerabilityHandle *db.VulnerabilityHandle Provider *db.Provider Related []any } func NewEntries(models ...any) []data.Entry { var entry RelatedEntries for i := range models { model := models[i] switch m := model.(type) { case db.VulnerabilityHandle: entry.VulnerabilityHandle = &m case db.AffectedPackageHandle, db.UnaffectedPackageHandle, db.AffectedCPEHandle, db.UnaffectedCPEHandle, db.KnownExploitedVulnerabilityHandle, db.EpssHandle, db.CWEHandle, db.OperatingSystemEOLHandle: entry.Related = append(entry.Related, m) case db.Provider: entry.Provider = &m default: panic(fmt.Sprintf("unsupported model type: %T", m)) } } return []data.Entry{ { DBSchemaVersion: db.ModelVersion, Data: entry, }, } } func (re RelatedEntries) String() string { var pkgs []string for _, r := range re.Related { switch v := r.(type) { case db.AffectedPackageHandle: pkgs = append(pkgs, v.Package.String()) case db.AffectedCPEHandle: pkgs = append(pkgs, fmt.Sprintf("%s/%s", v.CPE.Vendor, v.CPE.Product)) case db.KnownExploitedVulnerabilityHandle: pkgs = append(pkgs, "kev="+v.Cve) } } var fields []string if re.VulnerabilityHandle != nil { fields = append(fields, fmt.Sprintf("vuln=%q", re.VulnerabilityHandle.Name)) fields = append(fields, fmt.Sprintf("provider=%q", re.VulnerabilityHandle.ProviderID)) } else if re.Provider != nil { fields = append(fields, fmt.Sprintf("provider=%q", re.Provider.ID)) } fields = append(fields, fmt.Sprintf("entries=%d", len(re.Related))) return fmt.Sprintf("%s: %s", strings.Join(fields, " "), strings.Join(pkgs, ", ")) } ================================================ FILE: grype/db/v6/build/transformers/eol/transform.go ================================================ package eol import ( "strconv" "strings" "time" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/internal/log" ) // productNameMapping translates endoflife.date product names to grype distro names. // Only includes mappings where the names differ. var productNameMapping = map[string]string{ "alpine-linux": "alpine", "rhel": "redhat", "amazon-linux": "amazonlinux", "oracle-linux": "oraclelinux", "rocky-linux": "rockylinux", "centos-stream": "centos", // CentOS Stream is separate from classic CentOS } // supportedDistros lists distros we want to import EOL data for. // These are distros that grype tracks vulnerability data for. var supportedDistros = map[string]bool{ "alpine": true, "amazonlinux": true, "centos": true, "debian": true, "fedora": true, "oraclelinux": true, "redhat": true, "rockylinux": true, "almalinux": true, "sles": true, "ubuntu": true, "photon": true, "mariner": true, "azurelinux": true, "wolfi": true, "chainguard": true, } // Transform converts an EOL record into entries for the database. func Transform(entry unmarshal.EndOfLifeDateRelease, state provider.State) ([]data.Entry, error) { productName := entry.ProductName() distroName := translateProductName(productName) // Skip non-distro products (software packages, frameworks, etc.) if !supportedDistros[distroName] { log.WithFields("product", productName).Trace("skipping non-distro EOL record") return nil, nil } handle := getOperatingSystemEOL(entry, distroName) if handle == nil { return nil, nil } return transformers.NewEntries(*provider.Model(state), *handle), nil } // translateProductName converts endoflife.date product names to grype distro names. func translateProductName(product string) string { if mapped, ok := productNameMapping[product]; ok { return mapped } return product } // getOperatingSystemEOL creates an OperatingSystemEOLHandle from an EOL record. func getOperatingSystemEOL(entry unmarshal.EndOfLifeDateRelease, distroName string) *db.OperatingSystemEOLHandle { // Parse version from name (e.g., "12", "22.04", "8.5") majorVersion, minorVersion := parseVersion(entry.Name) // Parse EOL dates var eolDate, eoasDate *time.Time if entry.EOLFrom != nil { eolDate = internal.ParseTime(*entry.EOLFrom) } if entry.EOASFrom != nil { eoasDate = internal.ParseTime(*entry.EOASFrom) } // Skip if no EOL data if eolDate == nil && eoasDate == nil { return nil } // Note: We intentionally don't include codename in the handle because // endoflife.date uses full names like "Noble Numbat" while the DB uses // short lowercase names like "noble". Version matching is sufficient. return &db.OperatingSystemEOLHandle{ Name: distroName, MajorVersion: majorVersion, MinorVersion: minorVersion, EOLDate: eolDate, EOASDate: eoasDate, } } // parseVersion extracts major and minor version from a cycle string. // Normalizes versions by stripping leading zeros (e.g., "04" -> "4") // to match the format used in the vulnerability database. func parseVersion(cycle string) (major, minor string) { parts := strings.Split(cycle, ".") if len(parts) >= 1 { major = normalizeVersion(parts[0]) } if len(parts) >= 2 { minor = normalizeVersion(parts[1]) } return major, minor } // normalizeVersion strips leading zeros from a version string. func normalizeVersion(v string) string { // Try to parse as integer to strip leading zeros if i, err := strconv.Atoi(v); err == nil { return strconv.Itoa(i) } return v } ================================================ FILE: grype/db/v6/build/transformers/eol/transform_test.go ================================================ package eol import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseVersion(t *testing.T) { tests := []struct { name string cycle string wantMajor string wantMinor string }{ { name: "major only", cycle: "12", wantMajor: "12", wantMinor: "", }, { name: "major and minor", cycle: "22.04", wantMajor: "22", wantMinor: "4", }, { name: "major minor patch", cycle: "8.5.1", wantMajor: "8", wantMinor: "5", }, { name: "leading zero in minor", cycle: "20.04", wantMajor: "20", wantMinor: "4", }, { name: "leading zero in major", cycle: "08", wantMajor: "8", wantMinor: "", }, { name: "non-numeric version", cycle: "bullseye", wantMajor: "bullseye", wantMinor: "", }, { name: "mixed numeric and non-numeric", cycle: "3.x", wantMajor: "3", wantMinor: "x", }, { name: "empty string", cycle: "", wantMajor: "", wantMinor: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotMajor, gotMinor := parseVersion(tt.cycle) assert.Equal(t, tt.wantMajor, gotMajor, "major version mismatch") assert.Equal(t, tt.wantMinor, gotMinor, "minor version mismatch") }) } } func TestNormalizeVersion(t *testing.T) { tests := []struct { name string version string want string }{ { name: "no leading zeros", version: "12", want: "12", }, { name: "single leading zero", version: "04", want: "4", }, { name: "multiple leading zeros", version: "004", want: "4", }, { name: "zero value", version: "0", want: "0", }, { name: "non-numeric", version: "bullseye", want: "bullseye", }, { name: "mixed content", version: "12a", want: "12a", }, { name: "empty string", version: "", want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := normalizeVersion(tt.version) assert.Equal(t, tt.want, got) }) } } func TestTranslateProductName(t *testing.T) { tests := []struct { name string product string want string }{ { name: "alpine-linux to alpine", product: "alpine-linux", want: "alpine", }, { name: "rhel to redhat", product: "rhel", want: "redhat", }, { name: "amazon-linux to amazonlinux", product: "amazon-linux", want: "amazonlinux", }, { name: "oracle-linux to oraclelinux", product: "oracle-linux", want: "oraclelinux", }, { name: "rocky-linux to rockylinux", product: "rocky-linux", want: "rockylinux", }, { name: "centos-stream to centos", product: "centos-stream", want: "centos", }, { name: "unmapped product returns as-is", product: "debian", want: "debian", }, { name: "ubuntu returns as-is", product: "ubuntu", want: "ubuntu", }, { name: "unknown product returns as-is", product: "some-unknown-product", want: "some-unknown-product", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := translateProductName(tt.product) assert.Equal(t, tt.want, got) }) } } ================================================ FILE: grype/db/v6/build/transformers/epss/testdata/go-case.json ================================================ { "cve": "CVE-2025-0108", "epss": 0.328, "percentile": 0.9929, "date": "2025-02-18" } ================================================ FILE: grype/db/v6/build/transformers/epss/transform.go ================================================ package epss import ( "fmt" "time" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" ) func Transform(entry unmarshal.EPSS, state provider.State) ([]data.Entry, error) { date := internal.ParseTime(entry.Date) if date == nil { return nil, fmt.Errorf("failed to parse date: %q", entry.Date) } return transformers.NewEntries(*provider.Model(state), getEPSS(entry, *date)), nil } func getEPSS(entry unmarshal.EPSS, date time.Time) db.EpssHandle { return db.EpssHandle{ Cve: entry.CVE, Epss: entry.EPSS, Percentile: entry.Percentile, Date: date, } } ================================================ FILE: grype/db/v6/build/transformers/epss/transform_test.go ================================================ package epss import ( "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" ) func TestTransform(t *testing.T) { var timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) var listing = provider.File{ Path: "some", Digest: "123456", Algorithm: "sha256", } tests := []struct { name string want []transformers.RelatedEntries }{ { name: "testdata/go-case.json", want: []transformers.RelatedEntries{ { Provider: &db.Provider{ ID: "epss", Version: "12", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", }, Related: epssSlice( db.EpssHandle{ Cve: "CVE-2025-0108", Epss: 0.328, Percentile: 0.9929, Date: *internal.ParseTime("2025-02-18"), }, ), }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { entries := loadFixture(t, test.name) var actual []transformers.RelatedEntries for _, vuln := range entries { entries, err := Transform(vuln, provider.State{ Provider: "epss", Version: 12, Processor: "vunnel@1.2.3", Timestamp: timeVal, Listing: &listing, }) require.NoError(t, err) for _, entry := range entries { e, ok := entry.Data.(transformers.RelatedEntries) require.True(t, ok) actual = append(actual, e) } } if diff := cmp.Diff(test.want, actual); diff != "" { t.Errorf("data entries mismatch (-want +got):\n%s", diff) } }) } } func epssSlice(a ...db.EpssHandle) []any { var r []any for _, v := range a { r = append(r, v) } return r } func loadFixture(t *testing.T, fixturePath string) []unmarshal.EPSS { t.Helper() f, err := os.Open(fixturePath) require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.EPSSEntries(f) require.NoError(t, err) return entries } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/GHSA-2wgc-48g2-cj5w.json ================================================ { "Vulnerability": {}, "Advisory": { "Classification": "GENERAL", "Severity": "Medium", "CVSS": { "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N", "base_metrics": { "base_score": 6.5, "exploitability_score": 3.9, "impact_score": 2.5, "base_severity": "Medium" }, "status": "N/A" }, "FixedIn": [ { "name": "vantage6", "identifier": "4.2.0", "ecosystem": "python", "namespace": "github:python", "range": "< 4.2.0", "available": { "date": "2024-01-30T15:00:00Z", "kind": "advisory" } } ], "Summary": "vantage6 has insecure SSH configuration for node and server containers", "url": "https://github.com/advisories/GHSA-2wgc-48g2-cj5w", "CVE": [ "CVE-2024-21653" ], "Metadata": { "CVE": [ "CVE-2024-21653" ] }, "ghsaId": "GHSA-2wgc-48g2-cj5w", "published": "2024-01-30T20:56:46Z", "updated": "2024-02-08T22:48:31Z", "withdrawn": null, "namespace": "github:python" } } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/GHSA-3x74-v64j-qc3f.json ================================================ { "Vulnerability": {}, "Advisory": { "Classification": "GENERAL", "Severity": "HIGH", "CVSS": { "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", "base_metrics": { "base_score": 9.8, "exploitability_score": null, "impact_score": null, "base_severity": "HIGH" }, "status": "N/A" }, "FixedIn": [ { "name": "craftcms/cms", "identifier": "4.4.2", "ecosystem": "Packagist", "namespace": "github:Packagist", "range": "< 4.4.2" } ], "Summary": "Withdrawn Advisory: CraftCMS Server-Side Template Injection vulnerability", "url": "https://github.com/advisories/GHSA-3x74-v64j-qc3f", "CVE": [ "CVE-2023-30179" ], "Metadata": { "CVE": [ "CVE-2023-30179" ] }, "ghsaId": "GHSA-3x74-v64j-qc3f", "published": "2023-06-13T18:30:39Z", "updated": "2024-03-21T17:48:19Z", "withdrawn": "2023-06-28T23:54:39Z", "namespace": "github:Packagist" } } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/GHSA-92cp-5422-2mw7.json ================================================ { "Vulnerability": {}, "Advisory": { "Classification": "GENERAL", "Severity": "Low", "CVSS": { "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N", "base_metrics": { "base_score": 3.7, "exploitability_score": 2.2, "impact_score": 1.4, "base_severity": "Low" }, "status": "N/A" }, "FixedIn": [ { "name": "github.com/redis/go-redis/v9", "identifier": "9.7.3", "ecosystem": "go", "namespace": "github:go", "range": ">= 9.7.0-beta.1 < 9.7.3" }, { "name": "github.com/redis/go-redis/v9", "identifier": "9.6.3", "ecosystem": "go", "namespace": "github:go", "range": ">= 9.6.0b1 < 9.6.3" }, { "name": "github.com/redis/go-redis/v9", "identifier": "9.5.5", "ecosystem": "go", "namespace": "github:go", "range": ">= 9.5.1 < 9.5.5" } ], "Summary": "go-redis allows potential out of order responses when `CLIENT SETINFO` times out during connection establishment", "url": "https://github.com/advisories/GHSA-92cp-5422-2mw7", "CVE": [ "CVE-2025-29923" ], "Metadata": { "CVE": [ "CVE-2025-29923" ] }, "ghsaId": "GHSA-92cp-5422-2mw7", "published": "2025-03-20T18:49:59Z", "updated": "2025-03-20T18:50:01Z", "withdrawn": null, "namespace": "github:go" } } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/GHSA-qc55-vm3j-74gp.json ================================================ { "Vulnerability": {}, "Advisory": { "Classification": "GENERAL", "Severity": "High", "CVSS": { "version": "3.0", "vector_string": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N", "base_metrics": { "base_score": 5.5, "exploitability_score": 1.8, "impact_score": 3.6, "base_severity": "Medium" }, "status": "N/A" }, "cvss_severities": [ { "version": "3.0", "vector": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N" }, { "version": "4.0", "vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N" } ], "FixedIn": [ { "name": "jsnapy", "identifier": "1.3.0", "ecosystem": "python", "namespace": "github:python", "range": "< 1.3.0", "available": { "date": "2020-07-28", "kind": "first-observed" } } ], "Summary": "JSNAPy allows unprivileged local users to alter files under the directory", "url": "https://github.com/advisories/GHSA-qc55-vm3j-74gp", "CVE": [ "CVE-2018-0023" ], "Metadata": { "CVE": [ "CVE-2018-0023" ] }, "ghsaId": "GHSA-qc55-vm3j-74gp", "published": "2018-07-12T20:30:36Z", "updated": "2024-09-24T21:02:13Z", "withdrawn": null, "references": [ { "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-0023" }, { "url": "https://github.com/advisories/GHSA-qc55-vm3j-74gp" }, { "url": "https://kb.juniper.net/JSA10856" }, { "url": "https://github.com/pypa/advisory-database/tree/main/vulns/jsnapy/PYSEC-2018-84.yaml" }, { "url": "https://web.archive.org/web/20200227125151/http://www.securityfocus.com/bid/103745" } ], "namespace": "github:python" } } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/github-github-npm-0.json ================================================ { "Advisory": { "Classification": "GENERAL", "Severity": "Critical", "CVSS": { "version": "3.1", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "base_metrics": { "base_score": 9.8, "exploitability_score": 3.9, "impact_score": 5.9, "base_severity": "Critical" }, "status": "N/A" }, "FixedIn": [ { "name": "scratch-vm", "identifier": "0.2.0-prerelease.20200714185213", "ecosystem": "npm", "namespace": "github:npm", "range": "<= 0.2.0-prerelease.20200709173451" } ], "Summary": "Remote Code Execution in scratch-vm", "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", "CVE": [ "CVE-2020-14000" ], "Metadata": { "CVE": [ "CVE-2020-14000" ] }, "ghsaId": "GHSA-vc9j-fhvv-8vrf", "published": "2020-07-27T19:55:52Z", "updated": "2023-01-09T05:03:39Z", "withdrawn": null, "namespace": "github:npm" } } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/github-github-python-0.json ================================================ [ { "Advisory": { "CVE": [ "CVE-2018-8768" ], "FixedIn": [ { "ecosystem": "python", "identifier": "5.4.1", "name": "notebook", "namespace": "github:python", "range": "< 5.4.1" } ], "Metadata": { "CVE": [ "CVE-2018-8768" ] }, "Severity": "Low", "Summary": "Low severity vulnerability that affects notebook", "ghsaId": "GHSA-6cwv-x26c-w2q4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", "withdrawn": null }, "Vulnerability": {} }, { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "GHSA-p5wr-vp8g-q5p4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} } ] ================================================ FILE: grype/db/v6/build/transformers/github/testdata/github-withdrawn.json ================================================ { "Advisory": { "CVE": [ "CVE-2018-8768" ], "FixedIn": [ { "ecosystem": "python", "identifier": "5.4.1", "name": "notebook", "namespace": "github:python", "range": "< 5.4.1" } ], "Metadata": { "CVE": [ "CVE-2018-8768" ] }, "Severity": "Low", "Summary": "Low severity vulnerability that affects notebook", "ghsaId": "GHSA-6cwv-x26c-w2q4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", "withdrawn": "2022-01-31T14:32:09Z" }, "Vulnerability": {} } ================================================ FILE: grype/db/v6/build/transformers/github/testdata/multiple-fixed-in-names.json ================================================ { "Advisory": { "CVE": [ "CVE-2017-5524" ], "FixedIn": [ { "ecosystem": "python", "identifier": "4.3.12", "name": "Plone", "namespace": "github:python", "range": ">= 4.0 < 4.3.12", "available": { "date": "2017-05-20T10:30:45Z", "kind": "release" } }, { "ecosystem": "python", "identifier": "5.1b1", "name": "Plone", "namespace": "github:python", "range": ">= 5.1a1 < 5.1b1", "available": { "date": "2017-06-15T14:22:33Z", "kind": "commit" } }, { "ecosystem": "python", "identifier": "5.0.7", "name": "Plone-debug", "namespace": "github:python", "range": ">= 5.0rc1 < 5.0.7" } ], "Metadata": { "CVE": [ "CVE-2017-5524" ] }, "Severity": "Medium", "Summary": "Moderate severity vulnerability that affects Plone", "ghsaId": "GHSA-p5wr-vp8g-q5p4", "namespace": "github:python", "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", "withdrawn": null }, "Vulnerability": {} } ================================================ FILE: grype/db/v6/build/transformers/github/transform.go ================================================ package github import ( "sort" "strings" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/db/v6/name" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/pkg" ) func Transform(vulnerability unmarshal.GitHubAdvisory, state provider.State) ([]data.Entry, error) { ins := []any{ getVulnerability(vulnerability, state), } for _, a := range getAffectedPackage(vulnerability) { ins = append(ins, a) } return transformers.NewEntries(ins...), nil } func getVulnerability(vuln unmarshal.GitHubAdvisory, state provider.State) db.VulnerabilityHandle { return db.VulnerabilityHandle{ Name: vuln.Advisory.GhsaID, ProviderID: state.Provider, Provider: provider.Model(state), ModifiedDate: internal.ParseTime(vuln.Advisory.Updated), PublishedDate: internal.ParseTime(vuln.Advisory.Published), WithdrawnDate: internal.ParseTime(vuln.Advisory.Withdrawn), Status: getVulnStatus(vuln), BlobValue: &db.VulnerabilityBlob{ ID: vuln.Advisory.GhsaID, // it does not appear to be possible to get "credits" or any user information from the graphql API // for security advisories (see https://docs.github.com/en/graphql/reference/queries#securityadvisories), // thus assigner is left empty. Assigners: nil, Description: strings.TrimSpace(vuln.Advisory.Summary), References: getReferences(vuln), Aliases: getAliases(vuln), Severities: getSeverities(vuln), }, } } func getVulnStatus(vuln unmarshal.GitHubAdvisory) db.VulnerabilityStatus { if vuln.Advisory.Withdrawn == "" { return db.VulnerabilityActive } return db.VulnerabilityRejected } func getAffectedPackage(vuln unmarshal.GitHubAdvisory) []db.AffectedPackageHandle { var afs []db.AffectedPackageHandle groups := groupFixedIns(vuln) hasRangeErr := false for group, fixedIns := range groups { for _, fixedInEntry := range fixedIns { ranges, rangeErr := getRanges(fixedInEntry) if rangeErr != nil { hasRangeErr = true } afs = append(afs, db.AffectedPackageHandle{ Package: getPackage(group), BlobValue: &db.PackageBlob{ CVEs: getAliases(vuln), Ranges: ranges, }, }) } } // stable ordering sort.Sort(internal.ByAffectedPackage(afs)) if hasRangeErr { log.Warnf("for %s falling back to fuzzy matching on at least one constraint range", vuln.Advisory.GhsaID) } return afs } func getRanges(fixedInEntry unmarshal.GithubFixedIn) ([]db.Range, error) { fixedVersion := db.Version{ Type: getAffectedVersionFormat(fixedInEntry), Constraint: versionutil.EnforceSemVerConstraint(fixedInEntry.Range), } err := validateAffectedVersion(fixedVersion) if err != nil { log.Warnf("failed to validate affected version: %v", err) fixedVersion.Type = version.UnknownFormat.String() } return []db.Range{ { Version: fixedVersion, Fix: getFix(fixedInEntry), }, }, err } func validateAffectedVersion(v db.Version) error { versionFormat := version.ParseFormat(v.Type) c, err := version.GetConstraint(v.Constraint, versionFormat) if err != nil { return err } // ensure we can use this version format in a comparison ver := version.New("1.0.0", versionFormat) if err := ver.Validate(); err != nil { // don't have a good example to use here // TODO: we should consider finding a better way to do this without having to create a valid version for comparison return nil } _, err = c.Satisfied(ver) return err } func getAffectedVersionFormat(fixedInEntry unmarshal.GithubFixedIn) string { versionFormat := strings.ToLower(fixedInEntry.Ecosystem) if versionFormat == "pip" { versionFormat = "python" } return versionFormat } func getFix(fixedInEntry unmarshal.GithubFixedIn) *db.Fix { fixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Identifier) fixState := db.NotFixedStatus if len(fixedInVersion) > 0 { fixState = db.FixedStatus } var detail *db.FixDetail availability := getFixAvailability(fixedInEntry) if availability != nil { detail = &db.FixDetail{ Available: availability, } } return &db.Fix{ Version: fixedInVersion, State: fixState, Detail: detail, } } func getFixAvailability(fixedInEntry unmarshal.GithubFixedIn) *db.FixAvailability { if fixedInEntry.Available.Date == "" { return nil } t := internal.ParseTime(fixedInEntry.Available.Date) if t == nil { log.WithFields("date", fixedInEntry.Available.Date).Warn("unable to parse fix availability date") return nil } return &db.FixAvailability{ Date: t, Kind: fixedInEntry.Available.Kind, } } type groupIndex struct { name string ecosystem string } func groupFixedIns(vuln unmarshal.GitHubAdvisory) map[groupIndex][]unmarshal.GithubFixedIn { grouped := make(map[groupIndex][]unmarshal.GithubFixedIn) for _, fixedIn := range vuln.Advisory.FixedIn { g := groupIndex{ name: fixedIn.Name, ecosystem: fixedIn.Ecosystem, } grouped[g] = append(grouped[g], fixedIn) } return grouped } func getPackageType(ecosystem string) pkg.Type { ecosystem = strings.ToLower(ecosystem) switch ecosystem { case "composer": return pkg.PhpComposerPkg case "rust", "cargo": return pkg.RustPkg case "dart": return pkg.DartPubPkg case "nuget", ".net": return pkg.DotnetPkg case "go", "golang": return pkg.GoModulePkg case "maven", "java": return pkg.JavaPkg case "npm": return pkg.NpmPkg case "pypi", "python", "pip": return pkg.PythonPkg case "swift": return pkg.SwiftPkg case "rubygems", "ruby", "gem": return pkg.GemPkg case "erlang", "hex", "elixir": return pkg.HexPkg case "apk": return pkg.ApkPkg case "rpm": return pkg.RpmPkg case "deb": return pkg.DebPkg case "github-action": return pkg.GithubActionPkg } ty := pkg.TypeByName(ecosystem) if ty != pkg.UnknownPkg { return ty } log.Warnf("using unknown ecosystem intead of syft pkg type (this will probably cause issues when matching): %q", ecosystem) return pkg.Type(ecosystem) } func getPackage(group groupIndex) *db.Package { t := getPackageType(group.ecosystem) return &db.Package{ Name: name.Normalize(group.name, t), Ecosystem: string(t), } } func getSeverities(vulnerability unmarshal.GitHubAdvisory) []db.Severity { var severities []db.Severity // the string severity and CVSS is not necessarily correlated (nor is CVSS guaranteed to be provided // at all... see https://github.com/advisories/GHSA-xwg4-93c6-3h42 for example), so we need to keep them separate cleanSeverity := strings.ToLower(strings.TrimSpace(vulnerability.Advisory.Severity)) if cleanSeverity != "" { severities = append(severities, db.Severity{ // This is the string severity based off of CVSS v3 // see https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/about-the-github-advisory-database?learn=security_advisories&learnProduct=code-security#about-cvss-levels Scheme: db.SeveritySchemeCHML, Value: cleanSeverity, }) } // If the new CVSSSeverities field isn't populated, fallback to the old CVSS property if len(vulnerability.Advisory.CVSSSeverities) == 0 && vulnerability.Advisory.CVSS != nil { severities = append(severities, db.Severity{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: vulnerability.Advisory.CVSS.VectorString, Version: vulnerability.Advisory.CVSS.Version, }, }) } else { for _, cvss := range vulnerability.Advisory.CVSSSeverities { severities = append(severities, db.Severity{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: cvss.Vector, Version: cvss.Version, }, }) } } return severities } func getAliases(vulnerability unmarshal.GitHubAdvisory) (aliases []string) { aliases = append(aliases, vulnerability.Advisory.CVE...) return } func getReferences(vulnerability unmarshal.GitHubAdvisory) []db.Reference { // Capture the GitHub Advisory URL as the first reference refs := []db.Reference{ { URL: vulnerability.Advisory.URL, }, } for _, reference := range vulnerability.Advisory.References { clean := strings.TrimSpace(reference.URL) if clean == "" { continue } // TODO there is other info we could be capturing too (source) refs = append(refs, db.Reference{ URL: clean, }) } return transformers.DeduplicateReferences(refs) } ================================================ FILE: grype/db/v6/build/transformers/github/transform_test.go ================================================ package github import ( "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/syft/syft/pkg" ) func TestTransform(t *testing.T) { type counts struct { providerCount int vulnerabilityCount int affectedPackageCount int } tests := []struct { name string fixture string state provider.State wantCounts counts }{ { name: "multiple fixed versions for Plone", fixture: "testdata/multiple-fixed-in-names.json", state: provider.State{ Provider: "github", Version: 1, Timestamp: time.Date(2024, 03, 01, 12, 0, 0, 0, time.UTC), }, wantCounts: counts{ providerCount: 1, vulnerabilityCount: 1, affectedPackageCount: 3, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { advisories := loadFixture(t, tt.fixture) require.Len(t, advisories, 1, "expected exactly one advisory") advisory := advisories[0] entries, err := Transform(advisory, tt.state) require.NoError(t, err) require.Len(t, entries, 1, "expected exactly one data.Entry") entry := entries[0] require.NotNil(t, entry.Data) data, ok := entry.Data.(transformers.RelatedEntries) require.True(t, ok, "expected entry.Data to be of type RelatedEntries") require.NotNil(t, data.VulnerabilityHandle, "expected a VulnerabilityHandle") require.Equal(t, tt.wantCounts.vulnerabilityCount, 1) require.Len(t, data.Related, tt.wantCounts.affectedPackageCount, "unexpected number of related entries") }) } } func TestGetVulnerability(t *testing.T) { now := time.Date(2024, 03, 01, 12, 0, 0, 0, time.UTC) tests := []struct { name string expected []db.VulnerabilityHandle }{ { name: "testdata/GHSA-2wgc-48g2-cj5w.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-2wgc-48g2-cj5w", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, ModifiedDate: internal.ParseTime("2024-02-08T22:48:31Z"), PublishedDate: internal.ParseTime("2024-01-30T20:56:46Z"), WithdrawnDate: nil, Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-2wgc-48g2-cj5w", Description: "vantage6 has insecure SSH configuration for node and server containers", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-2wgc-48g2-cj5w", }, }, Aliases: []string{"CVE-2024-21653"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "medium", }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N", Version: "3.1", }, }, }, }, }, }, }, { name: "testdata/GHSA-3x74-v64j-qc3f.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-3x74-v64j-qc3f", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, ModifiedDate: internal.ParseTime("2024-03-21T17:48:19Z"), PublishedDate: internal.ParseTime("2023-06-13T18:30:39Z"), WithdrawnDate: internal.ParseTime("2023-06-28T23:54:39Z"), Status: db.VulnerabilityRejected, BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-3x74-v64j-qc3f", Description: "Withdrawn Advisory: CraftCMS Server-Side Template Injection vulnerability", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-3x74-v64j-qc3f", }, }, Aliases: []string{"CVE-2023-30179"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "high", }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", }, }, }, }, }, }, }, { name: "testdata/github-github-npm-0.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-vc9j-fhvv-8vrf", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, ModifiedDate: internal.ParseTime("2023-01-09T05:03:39Z"), PublishedDate: internal.ParseTime("2020-07-27T19:55:52Z"), WithdrawnDate: nil, Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-vc9j-fhvv-8vrf", Description: "Remote Code Execution in scratch-vm", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", }, }, Aliases: []string{"CVE-2020-14000"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "critical", }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", }, }, }, }, }, }, }, { name: "testdata/github-github-python-0.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-6cwv-x26c-w2q4", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-6cwv-x26c-w2q4", Description: "Low severity vulnerability that affects notebook", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", }, }, Aliases: []string{"CVE-2018-8768"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "low", }, }, }, }, { Name: "GHSA-p5wr-vp8g-q5p4", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-p5wr-vp8g-q5p4", Description: "Moderate severity vulnerability that affects Plone", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", }, }, Aliases: []string{"CVE-2017-5524"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "medium", }, }, }, }, }, }, { name: "testdata/github-withdrawn.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-6cwv-x26c-w2q4", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, ModifiedDate: nil, PublishedDate: nil, WithdrawnDate: internal.ParseTime("2022-01-31T14:32:09Z"), Status: db.VulnerabilityRejected, BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-6cwv-x26c-w2q4", Description: "Low severity vulnerability that affects notebook", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", }, }, Aliases: []string{"CVE-2018-8768"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "low", }, }, }, }, }, }, { name: "testdata/multiple-fixed-in-names.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-p5wr-vp8g-q5p4", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-p5wr-vp8g-q5p4", Description: "Moderate severity vulnerability that affects Plone", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", }, }, Aliases: []string{"CVE-2017-5524"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "medium", }, }, }, }, }, }, { name: "testdata/GHSA-qc55-vm3j-74gp.json", expected: []db.VulnerabilityHandle{ { Name: "GHSA-qc55-vm3j-74gp", ProviderID: "github", Provider: &db.Provider{ ID: "github", Version: "1", DateCaptured: &now, }, ModifiedDate: internal.ParseTime("2024-09-24T21:02:13Z"), PublishedDate: internal.ParseTime("2018-07-12T20:30:36Z"), WithdrawnDate: nil, Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-qc55-vm3j-74gp", Description: "JSNAPy allows unprivileged local users to alter files under the directory", References: []db.Reference{ { URL: "https://github.com/advisories/GHSA-qc55-vm3j-74gp", }, { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-0023", }, { URL: "https://kb.juniper.net/JSA10856", }, { URL: "https://github.com/pypa/advisory-database/tree/main/vulns/jsnapy/PYSEC-2018-84.yaml", }, { URL: "https://web.archive.org/web/20200227125151/http://www.securityfocus.com/bid/103745", }, }, Aliases: []string{"CVE-2018-0023"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "high", }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N", Version: "3.0", }, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N", Version: "4.0", }, }, }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { advisories := loadFixture(t, tt.name) var results []db.VulnerabilityHandle for _, advisory := range advisories { result := getVulnerability(advisory, provider.State{Provider: "github", Version: 1, Timestamp: now}) results = append(results, result) } if d := cmp.Diff(tt.expected, results); d != "" { t.Fatalf("unexpected result: %s", d) } }) } } func TestGetAffectedPackage(t *testing.T) { tests := []struct { name string expected []db.AffectedPackageHandle }{ { name: "testdata/GHSA-2wgc-48g2-cj5w.json", expected: []db.AffectedPackageHandle{ { Package: &db.Package{ Name: "vantage6", Ecosystem: "python", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2024-21653"}, Ranges: []db.Range{ { Version: db.Version{ Type: "python", Constraint: "<4.2.0", }, Fix: &db.Fix{ Version: "4.2.0", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2024-01-30T15:00:00Z"), Kind: "advisory", }, }, }, }, }, }, }, }, }, { name: "testdata/GHSA-3x74-v64j-qc3f.json", expected: []db.AffectedPackageHandle{ { Package: &db.Package{ Name: "craftcms/cms", Ecosystem: "packagist", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-30179"}, Ranges: []db.Range{ { Version: db.Version{ Type: "packagist", Constraint: "<4.4.2", }, Fix: &db.Fix{ Version: "4.4.2", State: db.FixedStatus, }, }, }, }, }, }, }, { name: "testdata/github-github-npm-0.json", expected: []db.AffectedPackageHandle{ { Package: &db.Package{ Name: "scratch-vm", Ecosystem: "npm", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-14000"}, Ranges: []db.Range{ { Version: db.Version{ Type: "npm", Constraint: "<=0.2.0-prerelease.20200709173451", }, Fix: &db.Fix{ Version: "0.2.0-prerelease.20200714185213", State: db.FixedStatus, }, }, }, }, }, }, }, { name: "testdata/github-github-python-0.json", expected: []db.AffectedPackageHandle{ { Package: &db.Package{ Ecosystem: "python", Name: "notebook", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-8768"}, Qualifiers: nil, Ranges: []db.Range{ { Version: db.Version{Type: "python", Constraint: "<5.4.1"}, Fix: &db.Fix{Version: "5.4.1", State: db.FixedStatus}, }, }, }, }, { Package: &db.Package{ Ecosystem: "python", Name: "Plone", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2017-5524"}, Ranges: []db.Range{ { Version: db.Version{Type: "python", Constraint: ">=4.0,<4.3.12"}, Fix: &db.Fix{Version: "4.3.12", State: db.FixedStatus}, }, }, }, }, }, }, { name: "testdata/multiple-fixed-in-names.json", expected: []db.AffectedPackageHandle{ { Package: &db.Package{ Name: "Plone", Ecosystem: "python", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2017-5524"}, Ranges: []db.Range{ { Version: db.Version{ Type: "python", Constraint: ">=4.0,<4.3.12", }, Fix: &db.Fix{ Version: "4.3.12", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2017-05-20T10:30:45Z"), Kind: "release", }, }, }, }, }, }, }, { Package: &db.Package{ Name: "Plone", Ecosystem: "python", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2017-5524"}, Ranges: []db.Range{ { Version: db.Version{ Type: "python", Constraint: ">=5.1a1,<5.1b1", }, Fix: &db.Fix{ Version: "5.1b1", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2017-06-15T14:22:33Z"), Kind: "commit", }, }, }, }, }, }, }, { Package: &db.Package{ Name: "Plone-debug", Ecosystem: "python", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2017-5524"}, Ranges: []db.Range{ { Version: db.Version{ Type: "python", Constraint: ">=5.0rc1,<5.0.7", }, Fix: &db.Fix{ Version: "5.0.7", State: db.FixedStatus, }, }, }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { advisories := loadFixture(t, tt.name) var results []db.AffectedPackageHandle for _, advisor := range advisories { result := getAffectedPackage(advisor) results = append(results, result...) } if d := cmp.Diff(tt.expected, results); d != "" { t.Fatalf("unexpected result: %s", d) } }) } } func TestGetPackageType(t *testing.T) { tests := []struct { ecosystem string expectedType pkg.Type }{ {"composer", pkg.PhpComposerPkg}, {"Composer", pkg.PhpComposerPkg}, // testing case insensitivity {"COMPOSER", pkg.PhpComposerPkg}, // testing case insensitivity {"rust", pkg.RustPkg}, {"cargo", pkg.RustPkg}, {"dart", pkg.DartPubPkg}, {"nuget", pkg.DotnetPkg}, {".net", pkg.DotnetPkg}, {"go", pkg.GoModulePkg}, {"golang", pkg.GoModulePkg}, {"maven", pkg.JavaPkg}, {"java", pkg.JavaPkg}, {"npm", pkg.NpmPkg}, {"pypi", pkg.PythonPkg}, {"python", pkg.PythonPkg}, {"pip", pkg.PythonPkg}, {"swift", pkg.SwiftPkg}, {"rubygems", pkg.GemPkg}, {"ruby", pkg.GemPkg}, {"gem", pkg.GemPkg}, {"apk", pkg.ApkPkg}, {"rpm", pkg.RpmPkg}, {"deb", pkg.DebPkg}, {"github-action", pkg.GithubActionPkg}, // test for unknown type fallback {"unknown-ecosystem", pkg.Type("unknown-ecosystem")}, {"", pkg.Type("")}, } for _, tc := range tests { t.Run(tc.ecosystem, func(t *testing.T) { gotType := getPackageType(tc.ecosystem) if gotType != tc.expectedType { t.Errorf("getPackageType(%q) = %v, want %v", tc.ecosystem, gotType, tc.expectedType) } }) } } func TestGetRanges(t *testing.T) { advisories := loadFixture(t, "testdata/GHSA-92cp-5422-2mw7.json") require.Len(t, advisories, 1) advisory := advisories[0] var ranges []db.Range expectedRanges := []db.Range{ { Version: db.Version{ Type: "go", Constraint: ">=9.7.0-beta.1,<9.7.3", }, Fix: &db.Fix{ Version: "9.7.3", State: db.FixedStatus, }, }, { Version: db.Version{ // important: this emits an unknown constraint type, // triggering fuzzy matching when the input is not // valid semver Type: "Unknown", Constraint: ">=9.6.0b1,<9.6.3", }, Fix: &db.Fix{ Version: "9.6.3", State: db.FixedStatus, }, }, { Version: db.Version{ Type: "go", Constraint: ">=9.5.1,<9.5.5", }, Fix: &db.Fix{ Version: "9.5.5", State: db.FixedStatus, }, }, } var errors []error for _, fixedIn := range advisory.Advisory.FixedIn { rng, err := getRanges(fixedIn) if err != nil { errors = append(errors, err) } ranges = append(ranges, rng...) } require.Equal(t, 1, len(errors)) if diff := cmp.Diff(expectedRanges, ranges); diff != "" { t.Errorf("getRanges() mismatch (-want +got):\n%s", diff) } } func TestGetFixAvailability(t *testing.T) { tests := []struct { name string fixture string expected map[string]*db.FixAvailability // keyed by package identifier for fixture-based testing }{ { name: "GHSA-2wgc-48g2-cj5w with advisory availability", fixture: "testdata/GHSA-2wgc-48g2-cj5w.json", expected: map[string]*db.FixAvailability{ "4.2.0": { Date: internal.ParseTime("2024-01-30T15:00:00Z"), Kind: "advisory", }, }, }, { name: "multiple-fixed-in-names with mixed availability", fixture: "testdata/multiple-fixed-in-names.json", expected: map[string]*db.FixAvailability{ "4.3.12": { Date: internal.ParseTime("2017-05-20T10:30:45Z"), Kind: "release", }, "5.1b1": { Date: internal.ParseTime("2017-06-15T14:22:33Z"), Kind: "commit", }, "5.0.7": nil, // no availability data in fixture }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { advisories := loadFixture(t, tt.fixture) require.Len(t, advisories, 1, "expected exactly one advisory") for _, fixedIn := range advisories[0].Advisory.FixedIn { result := getFixAvailability(fixedIn) expected := tt.expected[fixedIn.Identifier] if expected == nil { require.Nil(t, result, "expected nil availability for %s", fixedIn.Identifier) } else { require.NotNil(t, result, "expected non-nil availability for %s", fixedIn.Identifier) require.Equal(t, expected.Kind, result.Kind) require.Equal(t, expected.Date, result.Date) } } }) } // keep edge case test for scenarios not covered by fixtures t.Run("invalid date returns nil", func(t *testing.T) { fixedIn := unmarshal.GithubFixedIn{ Available: struct { Date string `json:"date,omitempty"` Kind string `json:"kind,omitempty"` }{ Date: "invalid-date", Kind: "commit", }, } result := getFixAvailability(fixedIn) require.Nil(t, result) }) } func TestGetFix(t *testing.T) { // fixture-based tests tests := []struct { name string fixture string expected map[string]*db.Fix // keyed by package identifier }{ { name: "GHSA-2wgc-48g2-cj5w with availability", fixture: "testdata/GHSA-2wgc-48g2-cj5w.json", expected: map[string]*db.Fix{ "4.2.0": { Version: "4.2.0", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2024-01-30T15:00:00Z"), Kind: "advisory", }, }, }, }, }, { name: "multiple-fixed-in-names with mixed availability", fixture: "testdata/multiple-fixed-in-names.json", expected: map[string]*db.Fix{ "4.3.12": { Version: "4.3.12", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2017-05-20T10:30:45Z"), Kind: "release", }, }, }, "5.1b1": { Version: "5.1b1", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2017-06-15T14:22:33Z"), Kind: "commit", }, }, }, "5.0.7": { Version: "5.0.7", State: db.FixedStatus, Detail: nil, // no availability data }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { advisories := loadFixture(t, tt.fixture) require.Len(t, advisories, 1, "expected exactly one advisory") for _, fixedIn := range advisories[0].Advisory.FixedIn { result := getFix(fixedIn) expected := tt.expected[fixedIn.Identifier] require.NotNil(t, expected, "no expected result for identifier %s", fixedIn.Identifier) if d := cmp.Diff(expected, result); d != "" { t.Fatalf("unexpected result for %s: %s", fixedIn.Identifier, d) } } }) } // keep edge case tests t.Run("no fix version and no availability", func(t *testing.T) { fixedIn := unmarshal.GithubFixedIn{ Identifier: "", Available: struct { Date string `json:"date,omitempty"` Kind string `json:"kind,omitempty"` }{}, } expected := &db.Fix{ Version: "", State: db.NotFixedStatus, Detail: nil, } result := getFix(fixedIn) if d := cmp.Diff(expected, result); d != "" { t.Fatalf("unexpected result: %s", d) } }) } func loadFixture(t *testing.T, path string) []unmarshal.GitHubAdvisory { f, err := os.Open(path) t.Cleanup(func() { require.NoError(t, f.Close()) }) require.NoError(t, err) entries, err := unmarshal.GitHubAdvisoryEntries(f) require.NoError(t, err) return entries } ================================================ FILE: grype/db/v6/build/transformers/internal/sort.go ================================================ package internal import db "github.com/anchore/grype/grype/db/v6" type ByAffectedPackage []db.AffectedPackageHandle func (a ByAffectedPackage) Len() int { return len(a) } func (a ByAffectedPackage) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByAffectedPackage) Less(i, j int) bool { return comparePackageHandles(a[i].Package, a[i].BlobValue.Ranges, a[j].Package, a[j].BlobValue.Ranges) } type ByUnaffectedPackage []db.UnaffectedPackageHandle func (a ByUnaffectedPackage) Len() int { return len(a) } func (a ByUnaffectedPackage) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByUnaffectedPackage) Less(i, j int) bool { return comparePackageHandles(a[i].Package, a[i].BlobValue.Ranges, a[j].Package, a[j].BlobValue.Ranges) } // comparePackageHandles compares two package handles by name, ecosystem, then version constraints func comparePackageHandles(pkg1 *db.Package, ranges1 []db.Range, pkg2 *db.Package, ranges2 []db.Range) bool { if pkg1.Name != pkg2.Name { return pkg1.Name < pkg2.Name } if pkg1.Ecosystem != pkg2.Ecosystem { return pkg1.Ecosystem < pkg2.Ecosystem } // compare version constraints for _, r1 := range ranges1 { for _, r2 := range ranges2 { if r1.Version.Constraint != r2.Version.Constraint { return r1.Version.Constraint < r2.Version.Constraint } } } return false } ================================================ FILE: grype/db/v6/build/transformers/internal/time.go ================================================ package internal import ( "strings" "time" "github.com/araddon/dateparse" "github.com/anchore/grype/internal/log" ) func ParseTime(s string) *time.Time { s = strings.TrimSpace(s) if s == "" { return nil } t, err := time.Parse(time.RFC3339, s) if err == nil { return &t } // check if the timezone information is missing and append UTC if needed if !strings.Contains(s, "Z") && !strings.Contains(s, "+") && !strings.Contains(s, "-") { s += "Z" t, err = time.Parse(time.RFC3339, s) if err == nil { t = t.UTC() return &t } } // handle formats with milliseconds but no timezone formats := []string{ "2006-01-02T15:04:05.000", "2006-01-02T15:04:05.000Z", } for _, format := range formats { t, err = time.Parse(format, s) if err == nil { t = t.UTC() return &t } } // handle a wide variety of other formats t, err = dateparse.ParseAny(s) if err == nil { t = t.UTC() return &t } log.WithFields("time", s).Warnf("could not parse time: %v", err) return nil } ================================================ FILE: grype/db/v6/build/transformers/internal/time_test.go ================================================ package internal import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestParseTime(t *testing.T) { tests := []struct { name string input string expected *time.Time }{ { name: "empty string", input: "", expected: nil, }, { name: "valid RFC3339 with Z", input: "2024-11-15T12:34:56Z", expected: func() *time.Time { t, _ := time.Parse(time.RFC3339, "2024-11-15T12:34:56Z") return &t }(), }, { name: "valid RFC3339 without Z", input: "2024-11-15T12:34:56", expected: func() *time.Time { t, _ := time.Parse(time.RFC3339, "2024-11-15T12:34:56Z") return &t }(), }, { name: "valid with milliseconds no timezone", input: "2024-11-15T12:34:56.789", expected: func() *time.Time { t, _ := time.Parse("2006-01-02T15:04:05.000", "2024-11-15T12:34:56.789") utc := t.UTC() return &utc }(), }, { name: "valid with milliseconds and Z", input: "2024-11-15T12:34:56.789Z", expected: func() *time.Time { t, _ := time.Parse("2006-01-02T15:04:05.000Z", "2024-11-15T12:34:56.789Z") utc := t.UTC() return &utc }(), }, { name: "valid dateparse format", input: "November 15, 2024 12:34 PM UTC", expected: func() *time.Time { t, _ := time.Parse(time.RFC3339, "2024-11-15T12:34:00Z") return &t }(), }, { name: "valid date only", input: "2024-11-15", expected: func() *time.Time { t, _ := time.Parse("2006-01-02", "2024-11-15") utc := t.UTC() return &utc }(), }, { name: "valid date with time", input: "2024-11-15 01:02:03", expected: func() *time.Time { t, _ := time.Parse(time.RFC3339, "2024-11-15T01:02:03Z") return &t }(), }, { name: "invalid time format", input: "invalid-time", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ParseTime(tt.input) if tt.expected == nil { require.Nil(t, result) } else { require.NotNil(t, result) require.Equal(t, tt.expected.UTC(), result.UTC()) } }) } } ================================================ FILE: grype/db/v6/build/transformers/kev/testdata/go-case.json ================================================ { "cveID": "CVE-2025-0108", "vendorProject": "Palo Alto Networks", "product": "PAN-OS", "vulnerabilityName": "Palo Alto Networks PAN-OS Authentication Bypass Vulnerability", "dateAdded": "2025-02-18", "shortDescription": "Palo Alto Networks PAN-OS contains an authentication bypass vulnerability in its management web interface. This vulnerability allows an unauthenticated attacker with network access to the management web interface to bypass the authentication normally required and invoke certain PHP scripts.", "requiredAction": "Apply mitigations per vendor instructions [https://www.vendor.com/instructions] or discontinue use of the product if mitigations are unavailable [https:\/\/www.vendor.com\/something-else].", "dueDate": "2025-03-11", "knownRansomwareCampaignUse": "Unknown", "notes": "https:\/\/security.paloaltonetworks.com\/CVE-2025-0108 ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-0108 ; remaining information", "cwes": [ "CWE-306" ] } ================================================ FILE: grype/db/v6/build/transformers/kev/transform.go ================================================ package kev import ( "regexp" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" ) func Transform(kev unmarshal.KnownExploitedVulnerability, state provider.State) ([]data.Entry, error) { return transformers.NewEntries(*provider.Model(state), getKev(kev)), nil } func getKev(kev unmarshal.KnownExploitedVulnerability) db.KnownExploitedVulnerabilityHandle { urls, notes := getURLs([]string{kev.ShortDescription, kev.RequiredAction}, kev.Notes) return db.KnownExploitedVulnerabilityHandle{ Cve: kev.CveID, BlobValue: &db.KnownExploitedVulnerabilityBlob{ Cve: kev.CveID, VendorProject: kev.VendorProject, Product: kev.Product, DateAdded: internal.ParseTime(kev.DateAdded), RequiredAction: kev.RequiredAction, DueDate: internal.ParseTime(kev.DueDate), KnownRansomwareCampaignUse: strings.ToLower(kev.KnownRansomwareCampaignUse), Notes: notes, CWEs: kev.CWEs, URLs: urls, }, } } var bracketURLPattern = regexp.MustCompile(`\[(https?://[^\]]+)\]`) func getURLs(aux []string, notes string) ([]string, string) { // let's keep the URLs we find in order but also deduplicate them since we're combining URLs from multiple sources urlSet := strset.New() var urls []string // add URLs from notes first... if notes != "" { parts := strings.Split(notes, ";") cleanedParts := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if strings.HasPrefix(strings.ToLower(part), "http") { url := part if !urlSet.Has(url) { urlSet.Add(url) urls = append(urls, url) } } else if part != "" { cleanedParts = append(cleanedParts, part) } } notes = strings.Join(cleanedParts, "; ") } // ...then add URLs from the other fields for _, text := range aux { matches := bracketURLPattern.FindAllStringSubmatch(text, -1) for _, match := range matches { if len(match) > 1 { url := match[1] if !urlSet.Has(url) { urlSet.Add(url) urls = append(urls, url) } } } } return urls, notes } ================================================ FILE: grype/db/v6/build/transformers/kev/transform_test.go ================================================ package kev import ( "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" ) func TestTransform(t *testing.T) { var timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) var listing = provider.File{ Path: "some", Digest: "123456", Algorithm: "sha256", } tests := []struct { name string want []transformers.RelatedEntries }{ { name: "testdata/go-case.json", want: []transformers.RelatedEntries{ { Provider: &db.Provider{ ID: "kev", Version: "12", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", }, Related: kevSlice( db.KnownExploitedVulnerabilityHandle{ Cve: "CVE-2025-0108", BlobValue: &db.KnownExploitedVulnerabilityBlob{ Cve: "CVE-2025-0108", VendorProject: "Palo Alto Networks", Product: "PAN-OS", DateAdded: internal.ParseTime("2025-02-18"), RequiredAction: "Apply mitigations per vendor instructions [https://www.vendor.com/instructions] or discontinue use of the product if mitigations are unavailable [https://www.vendor.com/something-else].", DueDate: internal.ParseTime("2025-03-11"), KnownRansomwareCampaignUse: "unknown", Notes: "remaining information", URLs: []string{ "https://security.paloaltonetworks.com/CVE-2025-0108", "https://nvd.nist.gov/vuln/detail/CVE-2025-0108", "https://www.vendor.com/instructions", "https://www.vendor.com/something-else", }, CWEs: []string{"CWE-306"}, }, }, ), }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { entries := loadFixture(t, test.name) var actual []transformers.RelatedEntries for _, vuln := range entries { entries, err := Transform(vuln, provider.State{ Provider: "kev", Version: 12, Processor: "vunnel@1.2.3", Timestamp: timeVal, Listing: &listing, }) require.NoError(t, err) for _, entry := range entries { e, ok := entry.Data.(transformers.RelatedEntries) require.True(t, ok) actual = append(actual, e) } } if diff := cmp.Diff(test.want, actual); diff != "" { t.Errorf("data entries mismatch (-want +got):\n%s", diff) } }) } } func kevSlice(a ...db.KnownExploitedVulnerabilityHandle) []any { var r []any for _, v := range a { r = append(r, v) } return r } func loadFixture(t *testing.T, fixturePath string) []unmarshal.KnownExploitedVulnerability { t.Helper() f, err := os.Open(fixturePath) require.NoError(t, err) defer func() { require.NoError(t, f.Close()) }() entries, err := unmarshal.KnownExploitedVulnerabilityEntries(f) require.NoError(t, err) return entries } ================================================ FILE: grype/db/v6/build/transformers/msrc/testdata/microsoft-msrc-0.json ================================================ [ { "cvss": { "base_score": 7.8, "temporal_score": 7, "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4493470", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", "https://support.microsoft.com/help/4493470" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4494440", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", "https://support.microsoft.com/help/4494440" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4503267", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", "https://support.microsoft.com/en-us/help/4503267" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4507460", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", "https://support.microsoft.com/help/4507460" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4512517", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", "https://support.microsoft.com/help/4512517" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4516044", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", "https://support.microsoft.com/help/4516044" ], "available": { "date": "2019-11-12", "kind": "advisory" } } ], "id": "CVE-2019-0671", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "High", "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", "vulnerable": [ "4480961", "4483229", "4487026", "4489882" ] }, { "cvss": { "base_score": 4.4, "temporal_score": 4, "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" }, "fixed_in": [ { "id": "4093119", "is_first": true, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4103723", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4284880", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4338814", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4343887", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4345418", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4457131", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4462917", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4467691", "is_first": false, "is_latest": false, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" ], "available": { "date": "2019-11-12", "kind": "advisory" } }, { "id": "4471321", "is_first": false, "is_latest": true, "links": [ "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" ], "available": { "date": "2019-11-12", "kind": "advisory" } } ], "id": "CVE-2018-8116", "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", "product": { "family": "Windows", "id": "10852", "name": "Windows 10 Version 1607 for 32-bit Systems" }, "severity": "Medium", "summary": "Microsoft Graphics Component Denial of Service Vulnerability", "vulnerable": [ "3213986", "4013429", "4015217", "4019472", "4022715", "4025339", "4034658", "4038782", "4041691", "4048953", "4053579", "4056890", "4074590", "4088787" ] } ] ================================================ FILE: grype/db/v6/build/transformers/msrc/transform.go ================================================ package msrc import ( "strings" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/db/v6/name" "github.com/anchore/syft/syft/pkg" ) func Transform(vulnerability unmarshal.MSRCVulnerability, state provider.State) ([]data.Entry, error) { ins := []any{ getVulnerability(vulnerability, state), } ins = append(ins, getAffectedPackage(vulnerability)) return transformers.NewEntries(ins...), nil } func getVulnerability(vuln unmarshal.MSRCVulnerability, state provider.State) db.VulnerabilityHandle { return db.VulnerabilityHandle{ Name: vuln.ID, ProviderID: state.Provider, Provider: provider.Model(state), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: vuln.ID, Description: strings.TrimSpace(vuln.Summary), References: getReferences(vuln), Severities: getSeverities(vuln), }, } } func getAffectedPackage(vuln unmarshal.MSRCVulnerability) db.AffectedPackageHandle { return db.AffectedPackageHandle{ Package: getPackage(vuln), BlobValue: &db.PackageBlob{ Ranges: getRanges(vuln), }, } } func getPackage(vuln unmarshal.MSRCVulnerability) *db.Package { return &db.Package{ Name: name.Normalize(vuln.Product.ID, pkg.KbPkg), Ecosystem: string(pkg.KbPkg), } } func getRanges(vuln unmarshal.MSRCVulnerability) []db.Range { // In anchore-enterprise windows analyzer, "base" represents unpatched windows images (images with no KBs) // If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches), // then the image must be vulnerable to the image. vuln.Vulnerable = append(vuln.Vulnerable, "base") return []db.Range{ { Version: db.Version{ Type: "kb", Constraint: versionutil.OrConstraints(vuln.Vulnerable...), }, Fix: getFix(vuln), }, } } func getFix(vuln unmarshal.MSRCVulnerability) *db.Fix { fixedInVersion, fixDetail := fixedInKB(vuln) fixState := db.FixedStatus if fixedInVersion == "" { fixState = db.NotFixedStatus } return &db.Fix{ Version: fixedInVersion, State: fixState, Detail: fixDetail, } } // fixedInKB finds the "latest" patch (KB id) amongst the available microsoft patches and returns it // if the "latest" patch cannot be found, an empty string is returned func fixedInKB(vulnerability unmarshal.MSRCVulnerability) (string, *db.FixDetail) { for _, fixedIn := range vulnerability.FixedIn { if fixedIn.IsLatest { var detail *db.FixDetail if fixedIn.Available.Date != "" { detail = &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime(fixedIn.Available.Date), Kind: fixedIn.Available.Kind, }, } } return fixedIn.ID, detail } } return "", nil } func getReferences(vuln unmarshal.MSRCVulnerability) []db.Reference { refs := []db.Reference{ { URL: vuln.Link, }, } return refs } func getSeverities(vuln unmarshal.MSRCVulnerability) []db.Severity { var severities []db.Severity cleanSeverity := strings.ToLower(strings.TrimSpace(vuln.Severity)) if cleanSeverity != "" { severities = append(severities, db.Severity{ Scheme: db.SeveritySchemeCHML, Value: cleanSeverity, }) } if vuln.Cvss.Vector != "" { severities = append(severities, db.Severity{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: vuln.Cvss.Vector, Version: "3.0", // TODO: assuming CVSS v3, update if different }, }) } return severities } ================================================ FILE: grype/db/v6/build/transformers/msrc/transform_test.go ================================================ package msrc import ( "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/testutil" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" ) func TestUnmarshalMsrcVulnerabilities(t *testing.T) { f, err := os.Open("testdata/microsoft-msrc-0.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.MSRCVulnerabilityEntries(f) require.NoError(t, err) assert.Equal(t, len(entries), 2) } func TestParseMSRCEntry(t *testing.T) { x := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) providerState := provider.State{ Provider: "msrc", Version: 1, DistributionVersion: 0, Processor: "", Schema: provider.Schema{}, URLs: nil, Timestamp: x, Listing: nil, Store: "", Stale: false, } expectedVulns := []data.Entry{ { DBSchemaVersion: db.ModelVersion, Data: transformers.RelatedEntries{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2019-0671", ProviderID: "msrc", Provider: &db.Provider{ ID: "msrc", Version: "1", DateCaptured: &x, }, Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2019-0671", Description: "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", References: []db.Reference{ { URL: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "high", }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C", Version: "3.0", }, }, }, }, }, Related: []any{ db.AffectedPackageHandle{ Package: &db.Package{ Name: "10852", Ecosystem: "msrc-kb", }, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{ Type: "kb", Constraint: `4480961 || 4483229 || 4487026 || 4489882 || base`, }, Fix: &db.Fix{ Version: "4516044", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timePtr(time.Date(2019, 11, 12, 0, 0, 0, 0, time.UTC)), Kind: "advisory", }, }, }, }, }, }, }, }, }, }, { DBSchemaVersion: db.ModelVersion, Data: transformers.RelatedEntries{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-8116", ProviderID: "msrc", Provider: &db.Provider{ ID: "msrc", Version: "1", DateCaptured: &x, }, Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-8116", Description: "Microsoft Graphics Component Denial of Service Vulnerability", References: []db.Reference{ { URL: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHML, Value: "medium", }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C", Version: "3.0", }, }, }, }, }, Related: []any{ db.AffectedPackageHandle{ Package: &db.Package{ Name: "10852", Ecosystem: "msrc-kb", }, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{ Type: "kb", Constraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`, }, Fix: &db.Fix{ Version: "4345418", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timePtr(time.Date(2019, 11, 12, 0, 0, 0, 0, time.UTC)), Kind: "advisory", }, }, }, }, }, }, }, }, }, }, } f, err := os.Open("testdata/microsoft-msrc-0.json") require.NoError(t, err) defer testutil.CloseFile(f) entries, err := unmarshal.MSRCVulnerabilityEntries(f) require.NoError(t, err) require.Equal(t, len(entries), 2) for idx, entry := range entries { dataEntries, err := Transform(entry, providerState) require.NoError(t, err) require.Len(t, dataEntries, 1, "expected a single data entry to be returned") if diff := cmp.Diff(expectedVulns[idx], dataEntries[0]); diff != "" { t.Errorf("data entry mismatch (-expected +actual):\n%s", diff) } } } func timePtr(t time.Time) *time.Time { return &t } ================================================ FILE: grype/db/v6/build/transformers/nvd/affected_range.go ================================================ package nvd import ( "fmt" "sort" "strings" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/syft/syft/cpe" ) type affectedRangeSet map[affectedCPERange]struct{} type affectedCPERange struct { ExactVersion string ExactUpdate string VersionStartIncluding string VersionStartExcluding string VersionEndIncluding string VersionEndExcluding string FixInfo *nvd.FixInfo } func newAffectedRanges(rs ...affectedCPERange) affectedRangeSet { s := make(affectedRangeSet) s.addRanges(rs...) return s } func newAffectedRange(match nvd.CpeMatch) affectedCPERange { return affectedCPERange{ VersionStartIncluding: nonEmptyValue(match.VersionStartIncluding), VersionStartExcluding: nonEmptyValue(match.VersionStartExcluding), VersionEndIncluding: nonEmptyValue(match.VersionEndIncluding), VersionEndExcluding: nonEmptyValue(match.VersionEndExcluding), FixInfo: match.Fix, } } func (s affectedRangeSet) addRanges(rs ...affectedCPERange) { for _, r := range rs { s[r] = struct{}{} } } func (s affectedRangeSet) toSlice() []affectedCPERange { var result []affectedCPERange for r := range s { result = append(result, r) } sort.Slice(result, func(i, j int) bool { if result[i].ExactVersion != result[j].ExactVersion { return result[i].ExactVersion < result[j].ExactVersion } if result[i].ExactUpdate != result[j].ExactUpdate { return result[i].ExactUpdate < result[j].ExactUpdate } if result[i].VersionStartIncluding != result[j].VersionStartIncluding { return result[i].VersionStartIncluding < result[j].VersionStartIncluding } if result[i].VersionStartExcluding != result[j].VersionStartExcluding { return result[i].VersionStartExcluding < result[j].VersionStartExcluding } if result[i].VersionEndIncluding != result[j].VersionEndIncluding { return result[i].VersionEndIncluding < result[j].VersionEndIncluding } if result[i].VersionEndExcluding != result[j].VersionEndExcluding { return result[i].VersionEndExcluding < result[j].VersionEndExcluding } return false }) return result } func (r affectedCPERange) String() string { constraints := make([]string, 0) if r.VersionStartIncluding != "" { constraints = append(constraints, fmt.Sprintf(">= %s", r.VersionStartIncluding)) } else if r.VersionStartExcluding != "" { constraints = append(constraints, fmt.Sprintf("> %s", r.VersionStartExcluding)) } if r.VersionEndExcluding != "" { constraints = append(constraints, fmt.Sprintf("< %s", r.VersionEndExcluding)) } else if r.VersionEndIncluding != "" { constraints = append(constraints, fmt.Sprintf("<= %s", r.VersionEndIncluding)) } if len(constraints) == 0 { version := r.ExactVersion update := r.ExactUpdate if version != cpe.Any && version != "-" { if update != cpe.Any && update != "-" { version = fmt.Sprintf("%s-%s", version, update) } constraints = append(constraints, fmt.Sprintf("= %s", version)) } } return strings.Join(constraints, ", ") } func nonEmptyValue(value *string) string { if value == nil { return "" } return *value } ================================================ FILE: grype/db/v6/build/transformers/nvd/affected_range_test.go ================================================ package nvd import ( "testing" "github.com/google/go-cmp/cmp" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" ) func Test_AffectedCPERange_String(t *testing.T) { tests := []struct { name string input affectedCPERange expected string }{ { name: "empty range", input: affectedCPERange{}, expected: "", }, { name: "exact version match", input: affectedCPERange{ ExactVersion: "1.0", }, expected: "= 1.0", }, { name: "exact version and update match", input: affectedCPERange{ ExactVersion: "1.0", ExactUpdate: "p1", }, expected: "= 1.0-p1", }, { name: "version start including only", input: affectedCPERange{ VersionStartIncluding: "1.0", }, expected: ">= 1.0", }, { name: "version start excluding only", input: affectedCPERange{ VersionStartExcluding: "1.0", }, expected: "> 1.0", }, { name: "version end including only", input: affectedCPERange{ VersionEndIncluding: "2.0", }, expected: "<= 2.0", }, { name: "version end excluding only", input: affectedCPERange{ VersionEndExcluding: "2.0", }, expected: "< 2.0", }, { name: "version range with start and end including", input: affectedCPERange{ VersionStartIncluding: "1.0", VersionEndIncluding: "2.0", }, expected: ">= 1.0, <= 2.0", }, { name: "version range with start including and end excluding", input: affectedCPERange{ VersionStartIncluding: "1.0", VersionEndExcluding: "2.0", }, expected: ">= 1.0, < 2.0", }, { name: "version range with start excluding and end including", input: affectedCPERange{ VersionStartExcluding: "1.0", VersionEndIncluding: "2.0", }, expected: "> 1.0, <= 2.0", }, { name: "version range with start and end excluding", input: affectedCPERange{ VersionStartExcluding: "1.0", VersionEndExcluding: "2.0", }, expected: "> 1.0, < 2.0", }, { name: "version range with all bounds (prefer outer bounds)", input: affectedCPERange{ VersionStartIncluding: "1.0", VersionStartExcluding: "0.9", VersionEndIncluding: "2.0", VersionEndExcluding: "2.1", }, expected: ">= 1.0, < 2.1", }, { name: "range constraints overrides exact version", input: affectedCPERange{ ExactVersion: "1.5", ExactUpdate: "p2", VersionStartIncluding: "1.0", VersionEndExcluding: "2.0", }, expected: ">= 1.0, < 2.0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := tt.input.String() if diff := cmp.Diff(tt.expected, actual); diff != "" { t.Errorf("buildConstraints() mismatch (-want +got):\n%s", diff) } }) } } func Test_newAffectedRange(t *testing.T) { tests := []struct { name string match nvd.CpeMatch expected affectedCPERange }{ { name: "basic range without fix info", match: nvd.CpeMatch{ VersionStartIncluding: stringPtr("1.0"), VersionEndExcluding: stringPtr("2.0"), }, expected: affectedCPERange{ VersionStartIncluding: "1.0", VersionEndExcluding: "2.0", FixInfo: nil, }, }, { name: "range with fix info", match: nvd.CpeMatch{ VersionStartIncluding: stringPtr("1.0"), VersionEndExcluding: stringPtr("2.0"), Fix: &nvd.FixInfo{ Version: "2.0", Date: "2023-06-15", Kind: "advisory", }, }, expected: affectedCPERange{ VersionStartIncluding: "1.0", VersionEndExcluding: "2.0", FixInfo: &nvd.FixInfo{ Version: "2.0", Date: "2023-06-15", Kind: "advisory", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := newAffectedRange(tt.match) if diff := cmp.Diff(tt.expected, actual); diff != "" { t.Errorf("newAffectedRange() mismatch (-want +got):\n%s", diff) } }) } } func stringPtr(s string) *string { return &s } ================================================ FILE: grype/db/v6/build/transformers/nvd/node.go ================================================ package nvd import ( "fmt" "sort" "strings" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) type affectedPackageCandidate struct { VulnerableCPE cpe.Attributes PlatformCPEs []cpe.Attributes Ranges affectedRangeSet } func allCandidates(cve string, configs []nvd.Configuration, cfg Config) ([]affectedPackageCandidate, error) { var candidates []affectedPackageCandidate for _, config := range configs { cs, err := processConfiguration(cve, config, cfg) if err != nil { return nil, err } candidates = append(candidates, cs...) } return deduplicateCandidates(candidates), nil } // processConfiguration processes a configuration recursively func processConfiguration(cve string, config nvd.Configuration, cfg Config) ([]affectedPackageCandidate, error) { var opPtr = config.Operator var op nvd.Operator if opPtr != nil { op = *opPtr } else { op = nvd.Or } if op == nvd.And { return processANDNodes(cve, config.Nodes, cfg, 0) } return processORNodes(cve, config.Nodes, cfg, 0) } // processANDNodes handles AND configurations func processANDNodes(cve string, nodes []nvd.Node, cfg Config, depth int) ([]affectedPackageCandidate, error) { depth++ if depth > 2 { log.WithFields("depth", depth, "cve", cve, "operator", "and").Warn("unexpected NVD node configuration depth") } var candidates []affectedPackageCandidate // find all vulnerable CPEs and all platform CPEs across all nodes var allVulnerableCPEs []affectedPackageCandidate var allPlatformCPEs []cpe.Attributes for _, node := range nodes { switch node.Operator { case nvd.Or: vulnCPEs, err := extractVulnerableCPEs(node, cfg) if err != nil { return nil, err } allVulnerableCPEs = append(allVulnerableCPEs, vulnCPEs...) platformCPEs, err := extractPlatformCPEs(node) if err != nil { return nil, err } allPlatformCPEs = append(allPlatformCPEs, platformCPEs...) case nvd.And: // TODO: when we're processing AND'd nodes at this depth this tends to mean that all the given CPEs must // be present in the environment for the vulnerability to be applicable. This isn't something we can // express as a single affected package in grype today. We should consider how to handle this case in // the future. var names []string for _, match := range node.CpeMatch { short := strings.ReplaceAll(strings.ReplaceAll(match.Criteria, ":*", ""), ":-", "") postfix := "" if !match.Vulnerable { postfix = " (not vulnerable)" } names = append(names, fmt.Sprintf("%q%s", short, postfix)) } log.WithFields("cve", cve, "criteria", strings.Join(names, " AND ")).Warnf("unsupported NVD node configuration (dropping criteria)") } } // deduplicate CPEs uniqueVulnCPEs := make(map[string]affectedPackageCandidate) for _, c := range allVulnerableCPEs { cKey := cpeKey(c.VulnerableCPE) if _, exists := uniqueVulnCPEs[cKey]; !exists { uniqueVulnCPEs[cKey] = c } else { uniqueVulnCPEs[cKey].Ranges.addRanges(c.Ranges.toSlice()...) } } // combine all unique vulnerable CPEs with their associated ranges for _, vulnCPE := range uniqueVulnCPEs { if len(allPlatformCPEs) == 0 { // no platform constraints, app is vulnerable on all platforms candidates = append(candidates, vulnCPE) } else { // associate this vulnerable CPE with all platform CPEs vulnCPE.PlatformCPEs = allPlatformCPEs candidates = append(candidates, vulnCPE) } } return candidates, nil } // processORNodes handles OR configurations func processORNodes(cve string, nodes []nvd.Node, cfg Config, depth int) ([]affectedPackageCandidate, error) { depth++ if depth > 2 { log.WithFields("depth", depth, "cve", cve, "operator", "or").Warnf("unexpected NVD node configuration depth") } var candidates []affectedPackageCandidate for _, node := range nodes { switch node.Operator { case nvd.And: andCandidates, err := processANDNodes(cve, []nvd.Node{node}, cfg, depth) if err != nil { return nil, err } candidates = append(candidates, andCandidates...) case nvd.Or: vulnCPEs, err := extractVulnerableCPEs(node, cfg) if err != nil { return nil, err } candidates = append(candidates, vulnCPEs...) } } return candidates, nil } func deduplicateCandidates(candidates []affectedPackageCandidate) []affectedPackageCandidate { candidateMap := make(map[string]*affectedPackageCandidate) for _, candidate := range candidates { key := cpeKey(candidate.VulnerableCPE) existing, exists := candidateMap[key] if !exists { newCandidate := candidate candidateMap[key] = &newCandidate continue } // merge platform CPEs... platformMap := make(map[string]struct{}) for _, platform := range existing.PlatformCPEs { platformKey := cpeKey(platform) platformMap[platformKey] = struct{}{} } for _, platform := range candidate.PlatformCPEs { platformKey := cpeKey(platform) if _, ok := platformMap[platformKey]; !ok { existing.PlatformCPEs = append(existing.PlatformCPEs, platform) platformMap[platformKey] = struct{}{} } } // merge ranges... existing.Ranges.addRanges(candidate.Ranges.toSlice()...) } var result []affectedPackageCandidate for _, candidate := range candidateMap { if len(candidate.Ranges) == 0 { candidate.Ranges.addRanges(deriveRangesFromCPE(candidate.VulnerableCPE)...) } result = append(result, *candidate) } // sort the slice for deterministic output sort.Slice(result, func(i, j int) bool { return result[i].VulnerableCPE.String() < result[j].VulnerableCPE.String() }) return result } func deriveRangesFromCPE(attr cpe.Attributes) []affectedCPERange { if attr.Version == cpe.Any { return nil } var update string if attr.Update != "-" { update = attr.Update } return []affectedCPERange{ { ExactVersion: attr.Version, ExactUpdate: update, }, } } // extractVulnerableCPEs extracts CPES that are both within the CPE part configuration and are explicitly marked as vulnerable func extractVulnerableCPEs(node nvd.Node, cfg Config) ([]affectedPackageCandidate, error) { var candidates []affectedPackageCandidate for _, match := range node.CpeMatch { if !match.Vulnerable { continue } cpeAttr, err := cpe.NewAttributes(match.Criteria) if err != nil { return nil, fmt.Errorf("unable to parse CPE '%s': %w", match.Criteria, err) } // check if this CPE part is in our configured set of parts to process, if not then it should not be considered // as an affected package at all if !cfg.CPEParts.Has(cpeAttr.Part) { continue } candidate := affectedPackageCandidate{ VulnerableCPE: cpeAttr, } if match.VersionStartIncluding != nil || match.VersionStartExcluding != nil || match.VersionEndIncluding != nil || match.VersionEndExcluding != nil { candidate.Ranges = newAffectedRanges(newAffectedRange(match)) } else { // no explicit version ranges in the match, check the CPE attributes for an exact version candidate.Ranges = newAffectedRanges(deriveRangesFromCPE(cpeAttr)...) } candidates = append(candidates, candidate) } return candidates, nil } // extractPlatformCPEs extracts all platform CPEs from a node (explicitly non-vulnerable CPEs). Why not just // use the part indication (i.e. 'h' & 'o' are platform and 'a' is the vulnerable candidate)? Because you can // find cases where an application is the platform (e.g. kubernetes or openshift). func extractPlatformCPEs(node nvd.Node) ([]cpe.Attributes, error) { var platformCPEs []cpe.Attributes for _, match := range node.CpeMatch { cpeAttr, err := cpe.NewAttributes(match.Criteria) if err != nil { return nil, fmt.Errorf("unable to parse CPE '%s': %w", match.Criteria, err) } if !match.Vulnerable { platformCPEs = append(platformCPEs, cpeAttr) } } return platformCPEs, nil } // cpeKey generates a unique key for a CPE (everything except for the version and update) func cpeKey(cpe cpe.Attributes) string { return fmt.Sprintf("%s|%s|%s|%s|%s|%s|%s|%s|%s", cpe.Part, cpe.Vendor, cpe.Product, cpe.Edition, cpe.SWEdition, cpe.TargetSW, cpe.TargetHW, cpe.Other, cpe.Language) } ================================================ FILE: grype/db/v6/build/transformers/nvd/node_test.go ================================================ package nvd import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" ) func TestDeduplicateCandidates(t *testing.T) { aVendorProduct1 := cpe.Attributes{ Part: "a", Vendor: "vendor1", Product: "product1", } aVendorProduct2 := cpe.Attributes{ Part: "a", Vendor: "vendor2", Product: "product2", } osProduct1 := cpe.Attributes{ Part: "o", Vendor: "os1", Product: "os1product", } osProduct2 := cpe.Attributes{ Part: "o", Vendor: "os2", Product: "os2product", } tests := []struct { name string input []affectedPackageCandidate expected []affectedPackageCandidate }{ { name: "empty input", input: []affectedPackageCandidate{}, expected: nil, }, { name: "go case", input: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, }, { name: "deduplicate identical candidates", input: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, }, { name: "merge ranges for same CPE", input: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "2.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges( affectedCPERange{ExactVersion: "1.0"}, affectedCPERange{ExactVersion: "2.0"}, ), }, }, }, { name: "merge platform CPEs for same vulnerable CPE", input: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, PlatformCPEs: []cpe.Attributes{ osProduct1, }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: aVendorProduct1, PlatformCPEs: []cpe.Attributes{ osProduct2, }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, PlatformCPEs: []cpe.Attributes{ osProduct1, osProduct2, }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, }, { name: "different CPEs not deduplicated", input: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: aVendorProduct2, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "2.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: aVendorProduct1, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: aVendorProduct2, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "2.0", }), }, }, }, { name: "deduplicate based on target software", input: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", TargetSW: "target1", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", TargetSW: "target2", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", TargetSW: "target1", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", TargetSW: "target2", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, }, { name: "derive ranges when none specified", input: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "3.0", Update: "p2", }, Ranges: newAffectedRanges(), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "3.0", Update: "p2", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "3.0", ExactUpdate: "p2", }), }, }, }, { name: "derive ranges for one candidate but not others", input: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product1", Version: "3.0", }, Ranges: newAffectedRanges(), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product2", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product1", Version: "3.0", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "3.0", }), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product2", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, }, { name: "complex case with mixed input", input: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "1.0", SWEdition: "enterprise", }, PlatformCPEs: []cpe.Attributes{ osProduct1, }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "1.0", SWEdition: "enterprise", }, PlatformCPEs: []cpe.Attributes{ osProduct2, }, Ranges: newAffectedRanges(affectedCPERange{ VersionStartIncluding: "1.0", VersionEndExcluding: "2.0", }), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "1.0", SWEdition: "community", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, }, expected: []affectedPackageCandidate{ { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "1.0", SWEdition: "community", }, Ranges: newAffectedRanges(affectedCPERange{ ExactVersion: "1.0", }), }, { VulnerableCPE: cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "1.0", SWEdition: "enterprise", }, PlatformCPEs: []cpe.Attributes{ osProduct1, osProduct2, }, Ranges: newAffectedRanges( affectedCPERange{ ExactVersion: "1.0", }, affectedCPERange{ VersionStartIncluding: "1.0", VersionEndExcluding: "2.0", }, ), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := deduplicateCandidates(tt.input) if diff := cmp.Diff(tt.expected, actual); diff != "" { t.Errorf("deduplicateCandidates() mismatch (-want +got):\n%s", diff) } }) } } func TestDeduplicateCandidates_SensitiveToAllCPEFields(t *testing.T) { base := cpe.Attributes{ Part: "a", Vendor: "vendor", Product: "product", Version: "1.0", Update: "update", Edition: "edition", SWEdition: "sw-edition", TargetSW: "target-sw", TargetHW: "target-hw", Language: "lang", Other: "other", } // note: we do not care about version and update fields for this part of the test... for field, mutate := range map[string]func(cpe.Attributes) cpe.Attributes{ "Part": func(c cpe.Attributes) cpe.Attributes { c.Part = "h"; return c }, "Vendor": func(c cpe.Attributes) cpe.Attributes { c.Vendor = "other-vendor"; return c }, "Product": func(c cpe.Attributes) cpe.Attributes { c.Product = "other-product"; return c }, "Edition": func(c cpe.Attributes) cpe.Attributes { c.Edition = "other-edition"; return c }, "SWEdition": func(c cpe.Attributes) cpe.Attributes { c.SWEdition = "other-sw-edition"; return c }, "TargetSW": func(c cpe.Attributes) cpe.Attributes { c.TargetSW = "other-target-sw"; return c }, "TargetHW": func(c cpe.Attributes) cpe.Attributes { c.TargetHW = "other-target-hw"; return c }, "Language": func(c cpe.Attributes) cpe.Attributes { c.Language = "other-lang"; return c }, "Other": func(c cpe.Attributes) cpe.Attributes { c.Other = "other-other"; return c }, } { t.Run("field="+field, func(t *testing.T) { a := affectedPackageCandidate{VulnerableCPE: base, Ranges: newAffectedRanges()} b := affectedPackageCandidate{VulnerableCPE: mutate(base), Ranges: newAffectedRanges()} result := deduplicateCandidates([]affectedPackageCandidate{a, b}) require.Len(t, result, 2, "field %s should cause deduplication to treat entries as separate", field) }) } // now that all other fields have been tested, prove that we do not care about version and update fields... t.Run("Version and Update do not matter", func(t *testing.T) { c1 := base c1.Version = "1.0" c1.Update = "u1" c2 := base c2.Version = "2.0" c2.Update = "u2" a := affectedPackageCandidate{VulnerableCPE: c1, Ranges: newAffectedRanges(affectedCPERange{ExactVersion: "1.0"})} b := affectedPackageCandidate{VulnerableCPE: c2, Ranges: newAffectedRanges(affectedCPERange{ExactVersion: "2.0"})} result := deduplicateCandidates([]affectedPackageCandidate{a, b}) require.Len(t, result, 1) require.Len(t, result[0].Ranges, 2) }) } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/CVE-2004-0377.json ================================================ { "cve": { "id": "CVE-2004-0377", "sourceIdentifier": "cve@mitre.org", "published": "2004-05-04T04:00:00.000", "lastModified": "2025-04-03T01:03:51.193", "vulnStatus": "Deferred", "cveTags": [], "descriptions": [ { "lang": "en", "value": "Buffer overflow in the win32_stat function for (1) ActiveState's ActivePerl and (2) Larry Wall's Perl before 5.8.3 allows local or remote attackers to execute arbitrary commands via filenames that end in a backslash character." }, { "lang": "es", "value": "Desbordamiento de búfer en la función win32_stat de \r\n\r\nActivePerl de ActiveState, y \r\nPerl de Larry Wall anterior a 5.8.3\r\n\r\npermite a atacantes remotos ejecutar comandos arbitrarios mediante nombres de fichero que terminan en un carácter \"\" (barra invertida)." } ], "metrics": {}, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "NVD-CWE-Other" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:activestate:activeperl:*:*:*:*:*:*:*:*", "matchCriteriaId": "86BADAC0-8A7B-4348-A78C-BAAFD8A784FE" }, { "vulnerable": true, "criteria": "cpe:2.3:a:larry_wall:perl:*:*:*:*:*:*:*:*", "versionEndIncluding": "5.8.3", "matchCriteriaId": "6851ACEC-141C-40B2-B6E1-CD52D979CE37" } ] } ] } ], "references": [] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/CVE-2008-3442.json ================================================ { "cve": { "id": "CVE-2008-3442", "sourceIdentifier": "cve@mitre.org", "published": "2008-08-01T14:41:00.000", "lastModified": "2008-09-05T21:43:05.500", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "desc." }, { "lang": "es", "value": "WinZip anterior a 11.0 no verifica adecuadamente la autenticidad de las actualizaciones, lo cual permite a atacantes de tipo 'hombre en el medio' (man-in-the-middle) ejecutar código de su elección a través de la actualización de un Caballo de Troya, que se manifiesta en el grado de daño y el envenenamiento de la caché DNS.\r\n\r\n" } ], "metrics": { "cvssMetricV2": [ ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-94" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:7.0:*:*:*:*:*:*:*", "matchCriteriaId": "A2ACBE01-B77A-4D09-8FB3-D6365786C44F" }, { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:8.0:*:*:*:*:*:*:*", "matchCriteriaId": "FDE7DCD6-90B3-4259-9BE6-B9F7A30A64AF" }, { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:8.1:*:*:*:*:*:*:*", "matchCriteriaId": "4088C545-249E-47AD-8BF8-A6A2E5B2BF18" }, { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:8.1:*:sr1:*:*:*:*:*", "matchCriteriaId": "FD308C7B-E9F6-4874-965D-E4271CF360DF" }, { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:9.0:*:*:*:*:*:*:*", "matchCriteriaId": "523ADB29-C3D5-4C06-89B6-22B5FC68C240" }, { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:9.0:*:sr1:*:*:*:*:*", "matchCriteriaId": "C79A7C70-F1CE-448B-B980-FB976609C48D" }, { "vulnerable": true, "criteria": "cpe:2.3:a:winzip:winzip:10.0:*:*:*:*:*:*:*", "matchCriteriaId": "B4FD09AC-2C56-4DB1-B00A-903103B453AD" } ] } ] } ], "references": [ ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-first.json ================================================ { "cve": { "id": "CVE-2023-45283", "sourceIdentifier": "security@golang.org", "published": "2023-11-09T17:15:08.757", "lastModified": "2023-12-14T10:15:07.947", "vulnStatus": "Modified", "cveTags": [], "descriptions": [ { "lang": "en", "value": "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored." }, { "lang": "es", "value": "El paquete filepath no reconoce las rutas con el prefijo \\??\\ como especiales. En Windows, una ruta que comienza con \\??\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\?\\. Se pueden utilizar rutas con un prefijo \\??\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\??\\c:\\x es equivalente a la ruta más común c:\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\a\\..\\??\\b en la ruta raíz del dispositivo local \\??\\b. Clean ahora convertirá esto a .\\??\\b. De manera similar, Join(\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\??\\b. Unirse ahora convertirá esto a \\.\\??\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\??\\ como absolutas, y VolumeName informa correctamente el prefijo \\??\\ como nombre de volumen." } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-22" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionEndExcluding": "1.20.11", "matchCriteriaId": "C1E7C289-7484-4AA8-A96B-07D2E2933258" }, { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionStartIncluding": "1.21.0-0", "versionEndExcluding": "1.21.4", "matchCriteriaId": "4E3FC16C-41B2-4900-901F-48BDA3DC9ED2" } ] } ] } ], "references": [ { "url": "http://www.openwall.com/lists/oss-security/2023/12/05/2", "source": "security@golang.org" }, { "url": "https://go.dev/cl/540277", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/cl/541175", "source": "security@golang.org" }, { "url": "https://go.dev/issue/63713", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/issue/64028", "source": "security@golang.org" }, { "url": "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", "source": "security@golang.org", "tags": [ "Issue Tracking", "Mailing List", "Vendor Advisory" ] }, { "url": "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", "source": "security@golang.org" }, { "url": "https://pkg.go.dev/vuln/GO-2023-2185", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20231214-0008/", "source": "security@golang.org" } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-last.json ================================================ { "cve": { "id": "CVE-2023-45283", "sourceIdentifier": "security@golang.org", "published": "2023-11-09T17:15:08.757", "lastModified": "2023-12-14T10:15:07.947", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored." }, { "lang": "es", "value": "El paquete filepath no reconoce las rutas con el prefijo \\??\\ como especiales. En Windows, una ruta que comienza con \\??\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\?\\. Se pueden utilizar rutas con un prefijo \\??\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\??\\c:\\x es equivalente a la ruta más común c:\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\a\\..\\??\\b en la ruta raíz del dispositivo local \\??\\b. Clean ahora convertirá esto a .\\??\\b. De manera similar, Join(\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\??\\b. Unirse ahora convertirá esto a \\.\\??\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\??\\ como absolutas, y VolumeName informa correctamente el prefijo \\??\\ como nombre de volumen." } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-22" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionEndExcluding": "1.20.11", "matchCriteriaId": "C1E7C289-7484-4AA8-A96B-07D2E2933258" }, { "vulnerable": true, "criteria": "cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*", "versionStartIncluding": "1.21.0-0", "versionEndExcluding": "1.21.4", "matchCriteriaId": "4E3FC16C-41B2-4900-901F-48BDA3DC9ED2" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] } ] } ], "references": [ { "url": "http://www.openwall.com/lists/oss-security/2023/12/05/2", "source": "security@golang.org" }, { "url": "https://go.dev/cl/540277", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/cl/541175", "source": "security@golang.org" }, { "url": "https://go.dev/issue/63713", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://go.dev/issue/64028", "source": "security@golang.org" }, { "url": "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", "source": "security@golang.org", "tags": [ "Issue Tracking", "Mailing List", "Vendor Advisory" ] }, { "url": "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", "source": "security@golang.org" }, { "url": "https://pkg.go.dev/vuln/GO-2023-2185", "source": "security@golang.org", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20231214-0008/", "source": "security@golang.org" } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/compound-pkg.json ================================================ { "cve": { "id": "CVE-2018-10189", "sourceIdentifier": "cve@mitre.org", "published": "2018-04-17T20:29:00.410", "lastModified": "2018-05-23T14:41:49.073", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." }, { "lang": "es", "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 5.0 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 10.0, "impactScore": 2.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-200" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", "versionStartIncluding": "1.0.0", "versionEndIncluding": "1.4.1", "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" }, { "vulnerable": true, "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", "versionStartIncluding": "2.0.0", "versionEndExcluding": "2.13.0", "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" } ] } ] } ], "references": [ { "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", "source": "cve@mitre.org", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/cve-2020-10729.json ================================================ { "cve": { "id": "CVE-2020-10729", "sourceIdentifier": "secalert@redhat.com", "published": "2021-05-27T19:15:07.880", "lastModified": "2021-12-10T19:57:06.357", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6." }, { "lang": "es", "value": "Se encontró un fallo en el uso de valores insuficientemente aleatorios en Ansible. Dos búsquedas de contraseñas aleatorias de la misma longitud generan el mismo valor que la acción de almacenamiento en caché de la plantilla para el mismo archivo, ya que no se realiza una reevaluación. La mayor amenaza de esta vulnerabilidad sería que todas las contraseñas estén expuestas a la vez para el archivo. Este fallo afecta a Ansible Engine versiones anteriores a 2.9.6" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", "attackVector": "LOCAL", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 5.5, "baseSeverity": "MEDIUM" }, "exploitabilityScore": 1.8, "impactScore": 3.6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:L/AC:L/Au:N/C:P/I:N/A:N", "accessVector": "LOCAL", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 2.1 }, "baseSeverity": "LOW", "exploitabilityScore": 3.9, "impactScore": 2.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-330" } ] }, { "source": "secalert@redhat.com", "type": "Secondary", "description": [ { "lang": "en", "value": "CWE-330" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*", "versionEndExcluding": "2.9.6", "matchCriteriaId": "EDFA8005-6FBE-4032-A499-608B7FA34F56" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*", "matchCriteriaId": "142AD0DD-4CF3-4D74-9442-459CE3347E3A" }, { "vulnerable": false, "criteria": "cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*", "matchCriteriaId": "F4CFF558-3C47-480D-A2F0-BABF26042943" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", "matchCriteriaId": "07B237A9-69A3-4A9C-9DA0-4E06BD37AE73" } ] } ] } ], "references": [ { "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1831089", "source": "secalert@redhat.com", "tags": [ "Issue Tracking", "Vendor Advisory" ] }, { "url": "https://github.com/ansible/ansible/issues/34144", "source": "secalert@redhat.com", "tags": [ "Exploit", "Issue Tracking", "Third Party Advisory" ] }, { "url": "https://www.debian.org/security/2021/dsa-4950", "source": "secalert@redhat.com", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/cve-2021-1566.json ================================================ { "cve": { "id": "CVE-2021-1566", "sourceIdentifier": "psirt@cisco.com", "published": "2021-06-16T18:15:08.710", "lastModified": "2024-11-21T05:44:38.237", "vulnStatus": "Modified", "cveTags": [], "descriptions": [ { "lang": "en", "value": "description." }, { "lang": "es", "value": "description" } ], "metrics": { "cvssMetricV31": [ { "source": "psirt@cisco.com", "type": "Secondary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1\/AV:N\/AC:H\/PR:N\/UI:N\/S:U\/C:H\/I:H\/A:N", "baseScore": 7.4, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "HIGH", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "NONE" }, "exploitabilityScore": 2.2, "impactScore": 5.2 }, { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1\/AV:N\/AC:H\/PR:N\/UI:N\/S:U\/C:H\/I:H\/A:N", "baseScore": 7.4, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "HIGH", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "NONE" }, "exploitabilityScore": 2.2, "impactScore": 5.2 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N\/AC:M\/Au:N\/C:P\/I:P\/A:N", "baseScore": 5.8, "accessVector": "NETWORK", "accessComplexity": "MEDIUM", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "NONE" }, "baseSeverity": "MEDIUM", "exploitabilityScore": 8.6, "impactScore": 4.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "psirt@cisco.com", "type": "Secondary", "description": [{ "lang": "en", "value": "CWE-296" }] }, { "source": "nvd@nist.gov", "type": "Primary", "description": [{ "lang": "en", "value": "CWE-295" }] } ], "configurations": [ { "nodes": [ { "operator": "AND", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:cisco:email_security_appliance:-:*:*:*:*:*:*:*", "matchCriteriaId": "678C2C6F-6D46-4BBE-A902-7AD031D8EBA8" }, { "vulnerable": true, "criteria": "cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*", "versionEndExcluding": "12.5.3-035", "matchCriteriaId": "6C3A8C94-CD5C-4309-8F1B-B151B3D091CC" } ] } ] }, { "nodes": [ { "operator": "AND", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:cisco:email_security_appliance:-:*:*:*:*:*:*:*", "matchCriteriaId": "678C2C6F-6D46-4BBE-A902-7AD031D8EBA8" }, { "vulnerable": true, "criteria": "cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*", "versionStartIncluding": "13.0", "versionEndExcluding": "13.0.0-030", "matchCriteriaId": "BE1DE406-EA9E-40DD-B18B-C19DF63EC13B" } ] } ] }, { "nodes": [ { "operator": "AND", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:cisco:email_security_appliance:-:*:*:*:*:*:*:*", "matchCriteriaId": "678C2C6F-6D46-4BBE-A902-7AD031D8EBA8" }, { "vulnerable": true, "criteria": "cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*", "versionStartIncluding": "13.5", "versionEndExcluding": "13.5.3-010", "matchCriteriaId": "39DEA2BD-4772-4F8D-9CD2-1BB377ECF64B" } ] } ] }, { "nodes": [ { "operator": "AND", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:cisco:web_security_appliance:-:*:*:*:*:*:*:*", "matchCriteriaId": "A7C2555C-7E97-475F-9EDC-027B51A40708" }, { "vulnerable": true, "criteria": "cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*", "versionEndExcluding": "11.8.3-021", "matchCriteriaId": "33FDC1BE-F1C3-4030-82CE-38D99DC30B5B" } ] } ] }, { "nodes": [ { "operator": "AND", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:cisco:web_security_appliance:-:*:*:*:*:*:*:*", "matchCriteriaId": "A7C2555C-7E97-475F-9EDC-027B51A40708" }, { "vulnerable": true, "criteria": "cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*", "versionStartIncluding": "12.0.0", "versionEndExcluding": "12.0.3-005", "matchCriteriaId": "D1CC6572-4281-45E1-9B33-6993B45E6B4F" } ] } ] }, { "nodes": [ { "operator": "AND", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:cisco:web_security_appliance:-:*:*:*:*:*:*:*", "matchCriteriaId": "A7C2555C-7E97-475F-9EDC-027B51A40708" }, { "vulnerable": true, "criteria": "cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*", "versionStartIncluding": "12.5.0", "versionEndExcluding": "12.5.1-043", "matchCriteriaId": "AA889DAF-1699-4A22-8A4C-D589F7BF10A8" } ] } ] } ], "references": [ { "url": "https:\/\/tools.cisco.com\/security\/center\/content\/CiscoSecurityAdvisory\/cisco-sa-esa-wsa-cert-vali-n8L97RW", "source": "psirt@cisco.com", "tags": ["Vendor Advisory"] }, { "url": "https:\/\/tools.cisco.com\/security\/center\/content\/CiscoSecurityAdvisory\/cisco-sa-esa-wsa-cert-vali-n8L97RW", "source": "af854a3a-2127-422b-91ae-364da2661108", "tags": ["Vendor Advisory"] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/cve-2022-0543.json ================================================ { "cve": { "id": "CVE-2022-0543", "sourceIdentifier": "security@debian.org", "published": "2022-02-18T20:15:17.583", "lastModified": "2023-09-29T15:55:24.533", "vulnStatus": "Analyzed", "cisaExploitAdd": "2022-03-28", "cisaActionDue": "2022-04-18", "cisaRequiredAction": "Apply updates per vendor instructions.", "cisaVulnerabilityName": "Debian-specific Redis Server Lua Sandbox Escape Vulnerability", "descriptions": [ { "lang": "en", "value": "It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution." }, { "lang": "es", "value": "Se ha detectado que redis, una base de datos persistente de valores clave, debido a un problema de empaquetado, es propenso a un escape del sandbox de Lua (específico de Debian), que podría resultar en una ejecución de código remota" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "CHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 10, "baseSeverity": "CRITICAL" }, "exploitabilityScore": 3.9, "impactScore": 6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:C/I:C/A:C", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "COMPLETE", "integrityImpact": "COMPLETE", "availabilityImpact": "COMPLETE", "baseScore": 10 }, "baseSeverity": "HIGH", "exploitabilityScore": 10, "impactScore": 10, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-862" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*", "matchCriteriaId": "5EBE5E1C-C881-4A76-9E36-4FB7C48427E6" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", "matchCriteriaId": "902B8056-9E37-443B-8905-8AA93E2447FB" }, { "vulnerable": false, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", "matchCriteriaId": "3D94DA3B-FA74-4526-A0A0-A872684598C6" }, { "vulnerable": false, "criteria": "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", "matchCriteriaId": "DEECE5FC-CACF-4496-A3E7-164736409252" }, { "vulnerable": false, "criteria": "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", "matchCriteriaId": "07B237A9-69A3-4A9C-9DA0-4E06BD37AE73" }, { "vulnerable": false, "criteria": "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", "matchCriteriaId": "FA6FEEC2-9F11-4643-8827-749718254FED" } ] } ] } ], "references": [ { "url": "http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html", "source": "security@debian.org", "tags": [ "Exploit", "Third Party Advisory", "VDB Entry" ] }, { "url": "https://bugs.debian.org/1005787", "source": "security@debian.org", "tags": [ "Issue Tracking", "Patch", "Third Party Advisory" ] }, { "url": "https://lists.debian.org/debian-security-announce/2022/msg00048.html", "source": "security@debian.org", "tags": [ "Mailing List", "Third Party Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20220331-0004/", "source": "security@debian.org", "tags": [ "Third Party Advisory" ] }, { "url": "https://www.debian.org/security/2022/dsa-5081", "source": "security@debian.org", "tags": [ "Mailing List", "Third Party Advisory" ] }, { "url": "https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce", "source": "security@debian.org", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/cve-2024-26663-standalone-os.json ================================================ { "cve": { "id": "CVE-2024-26663", "sourceIdentifier": "416baaa9-dc9f-4396-8d5f-8c081fb06d67", "published": "2024-04-02T07:15:43.287", "lastModified": "2025-01-07T17:20:30.367", "vulnStatus": "Analyzed", "cveTags": [], "descriptions": [ { "lang": "en", "value": "the description..." }, { "lang": "es", "value": "el description..." } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1\/AV:L\/AC:L\/PR:L\/UI:N\/S:U\/C:N\/I:N\/A:H", "baseScore": 5.5, "baseSeverity": "MEDIUM", "attackVector": "LOCAL", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "NONE", "integrityImpact": "NONE", "availabilityImpact": "HIGH" }, "exploitabilityScore": 1.8, "impactScore": 3.6 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [{ "lang": "en", "value": "CWE-476" }] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*", "versionStartIncluding": "4.9", "versionEndExcluding": "4.19.307", "matchCriteriaId": "A1A227E7-C02C-4FC4-84AA-230362C5E2C6" }, { "vulnerable": true, "criteria": "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*", "versionStartIncluding": "6.7", "versionEndExcluding": "6.7.5", "matchCriteriaId": "01925741-2C95-47C1-A7EA-3DC2BB0012D3" }, { "vulnerable": true, "criteria": "cpe:2.3:o:linux:linux_kernel:6.8:rc1:*:*:*:*:*:*", "matchCriteriaId": "B9F4EA73-0894-400F-A490-3A397AB7A517" }, { "vulnerable": true, "criteria": "cpe:2.3:o:linux:linux_kernel:6.8:rc2:*:*:*:*:*:*", "matchCriteriaId": "056BD938-0A27-4569-B391-30578B309EE3" }, { "vulnerable": true, "criteria": "cpe:2.3:o:linux:linux_kernel:6.8:rc3:*:*:*:*:*:*", "matchCriteriaId": "F02056A5-B362-4370-9FF8-6F0BD384D520" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", "matchCriteriaId": "07B237A9-69A3-4A9C-9DA0-4E06BD37AE73" } ] } ] } ], "references": [ { "url": "https:\/\/git.kernel.org\/stable\/c\/0cd331dfd6023640c9669d0592bc0fd491205f87", "source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67", "tags": ["Patch"] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/fix-version.json ================================================ { "cve": { "id": "CVE-2018-5487", "sourceIdentifier": "security-alert@netapp.com", "published": "2018-05-24T14:29:00.390", "lastModified": "2018-07-05T13:52:30.627", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." }, { "lang": "es", "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." } ], "metrics": { "cvssMetricV40": [ { "source": "security@zabbix.com", "type": "Secondary", "cvssData": { "version": "4.0", "vectorString": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", "baseScore": 7.5, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "HIGH", "attackRequirements": "NONE", "privilegesRequired": "NONE", "userInteraction": "ACTIVE", "vulnConfidentialityImpact": "HIGH", "vulnIntegrityImpact": "HIGH", "vulnAvailabilityImpact": "HIGH", "subConfidentialityImpact": "NONE", "subIntegrityImpact": "NONE", "subAvailabilityImpact": "NONE", "exploitMaturity": "NOT_DEFINED", "confidentialityRequirement": "NOT_DEFINED", "integrityRequirement": "NOT_DEFINED", "availabilityRequirement": "NOT_DEFINED", "modifiedAttackVector": "NOT_DEFINED", "modifiedAttackComplexity": "NOT_DEFINED", "modifiedAttackRequirements": "NOT_DEFINED", "modifiedPrivilegesRequired": "NOT_DEFINED", "modifiedUserInteraction": "NOT_DEFINED", "modifiedVulnConfidentialityImpact": "NOT_DEFINED", "modifiedVulnIntegrityImpact": "NOT_DEFINED", "modifiedVulnAvailabilityImpact": "NOT_DEFINED", "modifiedSubConfidentialityImpact": "NOT_DEFINED", "modifiedSubIntegrityImpact": "NOT_DEFINED", "modifiedSubAvailabilityImpact": "NOT_DEFINED", "Safety": "NOT_DEFINED", "Automatable": "NOT_DEFINED", "Recovery": "NOT_DEFINED", "valueDensity": "NOT_DEFINED", "vulnerabilityResponseEffort": "NOT_DEFINED", "providerUrgency": "NOT_DEFINED" } } ], "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 9.8, "baseSeverity": "CRITICAL" }, "exploitabilityScore": 3.9, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 7.5 }, "baseSeverity": "HIGH", "exploitabilityScore": 10.0, "impactScore": 6.4, "acInsufInfo": true, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-20" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", "versionStartIncluding": "7.2", "versionEndExcluding": "7.3", "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0", "fix": { "version": "7.3", "date": "2018-05-23", "kind": "advisory" } } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" } ] } ] } ], "references": [ { "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", "source": "security-alert@netapp.com", "tags": [ "Patch", "Vendor Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/fix-wrong-version.json ================================================ { "cve": { "id": "CVE-2018-5487", "sourceIdentifier": "security-alert@netapp.com", "published": "2018-05-24T14:29:00.390", "lastModified": "2018-07-05T13:52:30.627", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." }, { "lang": "es", "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." } ], "metrics": { "cvssMetricV40": [ { "source": "security@zabbix.com", "type": "Secondary", "cvssData": { "version": "4.0", "vectorString": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", "baseScore": 7.5, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "HIGH", "attackRequirements": "NONE", "privilegesRequired": "NONE", "userInteraction": "ACTIVE", "vulnConfidentialityImpact": "HIGH", "vulnIntegrityImpact": "HIGH", "vulnAvailabilityImpact": "HIGH", "subConfidentialityImpact": "NONE", "subIntegrityImpact": "NONE", "subAvailabilityImpact": "NONE", "exploitMaturity": "NOT_DEFINED", "confidentialityRequirement": "NOT_DEFINED", "integrityRequirement": "NOT_DEFINED", "availabilityRequirement": "NOT_DEFINED", "modifiedAttackVector": "NOT_DEFINED", "modifiedAttackComplexity": "NOT_DEFINED", "modifiedAttackRequirements": "NOT_DEFINED", "modifiedPrivilegesRequired": "NOT_DEFINED", "modifiedUserInteraction": "NOT_DEFINED", "modifiedVulnConfidentialityImpact": "NOT_DEFINED", "modifiedVulnIntegrityImpact": "NOT_DEFINED", "modifiedVulnAvailabilityImpact": "NOT_DEFINED", "modifiedSubConfidentialityImpact": "NOT_DEFINED", "modifiedSubIntegrityImpact": "NOT_DEFINED", "modifiedSubAvailabilityImpact": "NOT_DEFINED", "Safety": "NOT_DEFINED", "Automatable": "NOT_DEFINED", "Recovery": "NOT_DEFINED", "valueDensity": "NOT_DEFINED", "vulnerabilityResponseEffort": "NOT_DEFINED", "providerUrgency": "NOT_DEFINED" } } ], "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 9.8, "baseSeverity": "CRITICAL" }, "exploitabilityScore": 3.9, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 7.5 }, "baseSeverity": "HIGH", "exploitabilityScore": 10.0, "impactScore": 6.4, "acInsufInfo": true, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-20" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", "versionStartIncluding": "7.2", "versionEndExcluding": "7.3", "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0", "fix": { "version": "7.4", "date": "2018-05-23", "kind": "advisory" } } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" } ] } ] } ], "references": [ { "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", "source": "security-alert@netapp.com", "tags": [ "Patch", "Vendor Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/invalid_cpe.json ================================================ { "cve": { "id": "CVE-2015-8978", "sourceIdentifier": "cve@mitre.org", "published": "2016-11-22T17:59:00.180", "lastModified": "2016-11-28T19:50:59.600", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." }, { "lang": "es", "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "NONE", "integrityImpact": "NONE", "availabilityImpact": "HIGH", "baseScore": 7.5, "baseSeverity": "HIGH" }, "exploitabilityScore": 3.9, "impactScore": 3.6 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "NONE", "integrityImpact": "NONE", "availabilityImpact": "PARTIAL", "baseScore": 5.0 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 10.0, "impactScore": 2.9, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-399" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", "versionEndIncluding": "1.14", "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" } ] } ] } ], "references": [ { "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", "source": "cve@mitre.org", "tags": [ "Vendor Advisory" ] }, { "url": "http://www.securityfocus.com/bid/94487", "source": "cve@mitre.org" } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/jvm-packages.json ================================================ { "cve": { "id": "CVE-2023-JVM-TEST", "sourceIdentifier": "cve@mitre.org", "published": "2024-01-17T00:15:51.677", "lastModified": "2024-01-23T16:32:52.103", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "Test vulnerability affecting JVM packages to demonstrate version format detection." } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", "baseScore": 6.1, "baseSeverity": "MEDIUM", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "REQUIRED", "scope": "CHANGED", "confidentialityImpact": "LOW", "integrityImpact": "LOW", "availabilityImpact": "NONE" }, "exploitabilityScore": 2.8, "impactScore": 2.7 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-79" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:oracle:jdk:8u401:*:*:*:*:*:*:*", "matchCriteriaId": "oracle-jdk-8u401" }, { "vulnerable": true, "criteria": "cpe:2.3:a:oracle:jdk:11.0.22:*:*:*:*:*:*:*", "matchCriteriaId": "oracle-jdk-11.0.22" }, { "vulnerable": true, "criteria": "cpe:2.3:a:eclipse:openjdk:17.0.10:*:*:*:*:*:*:*", "matchCriteriaId": "eclipse-openjdk-17.0.10" }, { "vulnerable": true, "criteria": "cpe:2.3:a:azul:zulu:21.0.2:*:*:*:*:*:*:*", "matchCriteriaId": "azul-zulu-21.0.2" }, { "vulnerable": true, "criteria": "cpe:2.3:a:adoptium:java:17.0.10:*:*:*:*:*:*:*", "matchCriteriaId": "adoptium-java-17.0.10" } ] } ] } ], "references": [ { "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-JVM-TEST", "source": "nvd@nist.gov" }, { "url": "https://www.oracle.com/security-alerts/cpujan2024.html", "source": "nvd@nist.gov", "tags": ["Patch", "Vendor Advisory"] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/multiple-platforms-with-application-cpe.json ================================================ { "cve": { "id": "CVE-2023-38733", "sourceIdentifier": "psirt@us.ibm.com", "published": "2023-08-22T22:15:08.460", "lastModified": "2023-08-26T02:25:42.957", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "\nIBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs. IBM X-Force Id: 262293.\n\n" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 4.3, "baseSeverity": "MEDIUM" }, "exploitabilityScore": 2.8, "impactScore": 1.4 }, { "source": "psirt@us.ibm.com", "type": "Secondary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "NONE", "availabilityImpact": "NONE", "baseScore": 4.3, "baseSeverity": "MEDIUM" }, "exploitabilityScore": 2.8, "impactScore": 1.4 } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-532" } ] }, { "source": "psirt@us.ibm.com", "type": "Secondary", "description": [ { "lang": "en", "value": "CWE-532" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*", "versionStartIncluding": "21.0.0", "versionEndIncluding": "21.0.7.3", "matchCriteriaId": "DDF503DD-23DC-4B22-8873-BE94BF0F1CD1" }, { "vulnerable": true, "criteria": "cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*", "versionStartIncluding": "23.0.0", "versionEndIncluding": "23.0.3", "matchCriteriaId": "F513AA2B-F457-408B-8D5F-EBE657439000" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*", "matchCriteriaId": "F08E234C-BDCF-4B41-87B9-96BD5578CBBF" }, { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] } ] } ], "references": [ { "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/262293", "source": "psirt@us.ibm.com", "tags": [ "VDB Entry", "Vendor Advisory" ] }, { "url": "https://www.ibm.com/support/pages/node/7028223", "source": "psirt@us.ibm.com", "tags": [ "Patch", "Vendor Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/platform-cpe.json ================================================ { "cve": { "id": "CVE-2022-26488", "sourceIdentifier": "cve@mitre.org", "published": "2022-03-10T17:47:45.383", "lastModified": "2022-09-03T03:34:19.933", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2." }, { "lang": "es", "value": "En Python versiones anteriores a 3.10.3 en Windows, los usuarios locales pueden alcanzar privilegios porque la ruta de búsqueda no está asegurada apropiadamente. El instalador puede permitir a un atacante local añadir directorios escribibles por el usuario a la ruta de búsqueda del sistema. Para explotarla, un administrador debe haber instalado Python para todos los usuarios y habilitar las entradas PATH. Un usuario no administrador puede desencadenar una reparación que añada incorrectamente rutas escribibles por el usuario en el PATH, permitiendo el secuestro de la ruta de búsqueda de otros usuarios y servicios del sistema. Esto afecta a Python (CPython) versiones hasta 3.7.12, versiones 3.8.x hasta 3.8.12, versiones 3.9.x hasta 3.9.10, y versiones 3.10.x hasta 3.10.2" } ], "metrics": { "cvssMetricV31": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.1", "vectorString": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", "attackVector": "LOCAL", "attackComplexity": "HIGH", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 7, "baseSeverity": "HIGH" }, "exploitabilityScore": 1, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:L/AC:M/Au:N/C:P/I:P/A:P", "accessVector": "LOCAL", "accessComplexity": "MEDIUM", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 4.4 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 3.4, "impactScore": 6.4, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-426" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionEndIncluding": "3.7.12", "matchCriteriaId": "1E05F88A-70C2-4DB6-9CCC-1D599AD26D4C" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionStartIncluding": "3.8.0", "versionEndIncluding": "3.8.12", "matchCriteriaId": "E80CA0FB-E708-4E92-BF36-7267F799FF8D" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionStartIncluding": "3.9.0", "versionEndIncluding": "3.9.10", "matchCriteriaId": "DD4B9F29-F505-4721-A630-C75103942F29" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:*:*:*:*:*:*:*:*", "versionStartIncluding": "3.10.0", "versionEndIncluding": "3.10.2", "matchCriteriaId": "D5B55D1D-031C-4006-A368-BB66C2057916" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha1:*:*:*:*:*:*", "matchCriteriaId": "514A577E-5E60-40BA-ABD0-A8C5EB28BD90" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha2:*:*:*:*:*:*", "matchCriteriaId": "83B71795-9C81-4E5F-967C-C11808F24B05" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha3:*:*:*:*:*:*", "matchCriteriaId": "3F6F71F3-299E-4A4B-ADD1-EAD5A1D433E2" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*", "matchCriteriaId": "09BBF4E9-EA54-41B5-948E-8E3D2660B7EF" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*", "matchCriteriaId": "D9BBF4E9-EA54-41B5-948E-8E3D2660B7EF" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha5:*:*:*:*:*:*", "matchCriteriaId": "AEBFDCE7-81D4-4741-BB88-12C704515F5C" }, { "vulnerable": true, "criteria": "cpe:2.3:a:python:python:3.11.0:alpha6:*:*:*:*:*:*", "matchCriteriaId": "156EB4C2-EFB7-4CEB-804D-93DB62992A63" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", "matchCriteriaId": "A2572D17-1DE6-457B-99CC-64AFD54487EA" } ] } ] }, { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:active_iq_unified_manager:-:*:*:*:*:windows:*:*", "matchCriteriaId": "B55E8D50-99B4-47EC-86F9-699B67D473CE" }, { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:ontap_select_deploy_administration_utility:-:*:*:*:*:*:*:*", "matchCriteriaId": "E7CF3019-975D-40BB-A8A4-894E62BD3797" } ] } ] } ], "references": [ { "url": "https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/", "source": "cve@mitre.org", "tags": [ "Patch", "Vendor Advisory" ] }, { "url": "https://security.netapp.com/advisory/ntap-20220419-0005/", "source": "cve@mitre.org", "tags": [ "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/single-package-multi-distro.json ================================================ { "cve": { "id": "CVE-2018-1000222", "sourceIdentifier": "cve@mitre.org", "published": "2018-08-20T20:29:01.347", "lastModified": "2020-03-31T02:15:12.667", "vulnStatus": "Modified", "descriptions": [ { "lang": "en", "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." }, { "lang": "es", "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." } ], "metrics": { "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "REQUIRED", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 8.8, "baseSeverity": "HIGH" }, "exploitabilityScore": 2.8, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "MEDIUM", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 6.8 }, "baseSeverity": "MEDIUM", "exploitabilityScore": 8.6, "impactScore": 6.4, "acInsufInfo": false, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": true } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-415" } ] } ], "configurations": [ { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" }, { "vulnerable": true, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" }, { "vulnerable": true, "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" } ] } ] }, { "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" } ] } ] } ], "references": [ { "url": "https://github.com/libgd/libgd/issues/447", "source": "cve@mitre.org", "tags": [ "Issue Tracking", "Third Party Advisory" ] }, { "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "source": "cve@mitre.org", "tags": [ "Mailing List", "Third Party Advisory" ] }, { "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "source": "cve@mitre.org" }, { "url": "https://security.gentoo.org/glsa/201903-18", "source": "cve@mitre.org", "tags": [ "Third Party Advisory" ] }, { "url": "https://usn.ubuntu.com/3755-1/", "source": "cve@mitre.org", "tags": [ "Mitigation", "Third Party Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/testdata/version-range.json ================================================ { "cve": { "id": "CVE-2018-5487", "sourceIdentifier": "security-alert@netapp.com", "published": "2018-05-24T14:29:00.390", "lastModified": "2018-07-05T13:52:30.627", "vulnStatus": "Analyzed", "descriptions": [ { "lang": "en", "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." }, { "lang": "es", "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." } ], "metrics": { "cvssMetricV40": [ { "source": "security@zabbix.com", "type": "Secondary", "cvssData": { "version": "4.0", "vectorString": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", "baseScore": 7.5, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "HIGH", "attackRequirements": "NONE", "privilegesRequired": "NONE", "userInteraction": "ACTIVE", "vulnConfidentialityImpact": "HIGH", "vulnIntegrityImpact": "HIGH", "vulnAvailabilityImpact": "HIGH", "subConfidentialityImpact": "NONE", "subIntegrityImpact": "NONE", "subAvailabilityImpact": "NONE", "exploitMaturity": "NOT_DEFINED", "confidentialityRequirement": "NOT_DEFINED", "integrityRequirement": "NOT_DEFINED", "availabilityRequirement": "NOT_DEFINED", "modifiedAttackVector": "NOT_DEFINED", "modifiedAttackComplexity": "NOT_DEFINED", "modifiedAttackRequirements": "NOT_DEFINED", "modifiedPrivilegesRequired": "NOT_DEFINED", "modifiedUserInteraction": "NOT_DEFINED", "modifiedVulnConfidentialityImpact": "NOT_DEFINED", "modifiedVulnIntegrityImpact": "NOT_DEFINED", "modifiedVulnAvailabilityImpact": "NOT_DEFINED", "modifiedSubConfidentialityImpact": "NOT_DEFINED", "modifiedSubIntegrityImpact": "NOT_DEFINED", "modifiedSubAvailabilityImpact": "NOT_DEFINED", "Safety": "NOT_DEFINED", "Automatable": "NOT_DEFINED", "Recovery": "NOT_DEFINED", "valueDensity": "NOT_DEFINED", "vulnerabilityResponseEffort": "NOT_DEFINED", "providerUrgency": "NOT_DEFINED" } } ], "cvssMetricV30": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "3.0", "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "HIGH", "integrityImpact": "HIGH", "availabilityImpact": "HIGH", "baseScore": 9.8, "baseSeverity": "CRITICAL" }, "exploitabilityScore": 3.9, "impactScore": 5.9 } ], "cvssMetricV2": [ { "source": "nvd@nist.gov", "type": "Primary", "cvssData": { "version": "2.0", "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "accessVector": "NETWORK", "accessComplexity": "LOW", "authentication": "NONE", "confidentialityImpact": "PARTIAL", "integrityImpact": "PARTIAL", "availabilityImpact": "PARTIAL", "baseScore": 7.5 }, "baseSeverity": "HIGH", "exploitabilityScore": 10.0, "impactScore": 6.4, "acInsufInfo": true, "obtainAllPrivilege": false, "obtainUserPrivilege": false, "obtainOtherPrivilege": false, "userInteractionRequired": false } ] }, "weaknesses": [ { "source": "nvd@nist.gov", "type": "Primary", "description": [ { "lang": "en", "value": "CWE-20" } ] } ], "configurations": [ { "operator": "AND", "nodes": [ { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": true, "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", "versionStartIncluding": "7.2", "versionEndIncluding": "7.3", "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" } ] }, { "operator": "OR", "negate": false, "cpeMatch": [ { "vulnerable": false, "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" } ] } ] } ], "references": [ { "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", "source": "security-alert@netapp.com", "tags": [ "Patch", "Vendor Advisory" ] } ] } } ================================================ FILE: grype/db/v6/build/transformers/nvd/transform.go ================================================ package nvd import ( "regexp" "sort" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) var cwePattern = regexp.MustCompile(`^CWE-\d+$`) type Config struct { CPEParts *strset.Set InferNVDFixVersions bool } func defaultConfig() Config { return Config{ CPEParts: strset.New("a", "h", "o"), InferNVDFixVersions: true, } } func getVersionFormat(cpeProduct string) string { if pkg.HasJvmPackageName(cpeProduct) { return "jvm" } return "" } func Transformer(cfg Config) data.NVDTransformerV2 { if cfg == (Config{}) { cfg = defaultConfig() } return func(vulnerability unmarshal.NVDVulnerability, state provider.State) ([]data.Entry, error) { return transform(cfg, vulnerability, state) } } func transform(cfg Config, vulnerability unmarshal.NVDVulnerability, state provider.State) ([]data.Entry, error) { in := []any{ db.VulnerabilityHandle{ Name: vulnerability.ID, ProviderID: state.Provider, Provider: provider.Model(state), ModifiedDate: internal.ParseTime(vulnerability.LastModified), PublishedDate: internal.ParseTime(vulnerability.Published), Status: getVulnStatus(vulnerability), BlobValue: &db.VulnerabilityBlob{ ID: vulnerability.ID, Assigners: getAssigner(vulnerability), Description: strings.TrimSpace(vulnerability.Description()), References: getReferences(vulnerability), Severities: getSeverities(vulnerability), }, }, } for _, a := range getAffected(cfg, vulnerability) { in = append(in, a) } for _, cwe := range getCWEs(vulnerability) { in = append(in, cwe) } return transformers.NewEntries(in...), nil } func getAssigner(vuln unmarshal.NVDVulnerability) []string { if vuln.SourceIdentifier == nil { return nil } assigner := *vuln.SourceIdentifier if assigner == "" { return nil } return []string{assigner} } func getVulnStatus(vuln unmarshal.NVDVulnerability) db.VulnerabilityStatus { if vuln.VulnStatus == nil { return db.UnknownVulnerabilityStatus } // TODO: there is no path for withdrawn? // based off of the NVD or CVE list status, set the current vulnerability record status // see https://nvd.nist.gov/vuln/vulnerability-status s := strings.TrimSpace(strings.ReplaceAll(strings.ToLower(*vuln.VulnStatus), " ", "")) switch s { case "reserved", "received": // reserved (CVE list): A CVE Entry is marked as "RESERVED" when it has been reserved for use by a CVE Numbering Authority (CNA) or security // researcher, but the details of it are not yet populated. A CVE Entry can change from the RESERVED state to being populated at any time // based on a number of factors both internal and external to the CVE List. // // received (NVD): CVE has been recently published to the CVE List and has been received by the NVD. // return db.UnknownVulnerabilityStatus case "awaitinganalysis", "undergoinganalysis": // awaiting analysis (NVD): CVE has been marked for Analysis. Normally once in this state the CVE will be analyzed by NVD staff within 24 hours. // // undergoing analysis (NVD): CVE has been marked for Analysis. Normally once in this state the CVE will be analyzed by NVD staff within 24 hours. // return db.VulnerabilityAnalyzing case "disputed": // disputed (CVE list): When one party disagrees with another party's assertion that a particular issue in software is a vulnerability, a CVE Entry assigned // to that issue may be designated as being "DISPUTED". In these cases, CVE is making no determination as to which party is correct. Instead, we make // note of this dispute and try to offer any public references that will better inform those trying to understand the facts of the issue. // When you see a CVE Entry that is "DISPUTED", we encourage you to research the issue through the references or by contacting the affected // vendor or developer for more information. // return db.VulnerabilityDisputed case "rejected", "reject": // reject (CVE list): A CVE Entry listed as "REJECT" is a CVE Entry that is not accepted as a CVE Entry. The reason a CVE Entry is marked // REJECT will most often be stated in the description of the CVE Entry. Possible examples include it being a duplicate CVE Entry, it being // withdrawn by the original requester, it being assigned incorrectly, or some other administrative reason. // As a rule, REJECT CVE Entries should be ignored. // // rejected (NVD): CVE has been marked as "**REJECT**" in the CVE List. These CVEs are stored in the NVD, but do not show up in search results. return db.VulnerabilityRejected case "modified", "analyzed", "published": // modified (NVD): CVE has been amended by a source (CVE Primary CNA or another CNA). Analysis data supplied by the NVD may be no longer be accurate due to these changes. // // analyzed (NVD): CVE has had analysis completed and all data associations made. Each Analysis has three sub-types, Initial, Modified and Reanalysis: // Initial: Used to show the first time analysis was performed on a given CVE. // Modified: Used to show that analysis was performed due to a modification the CVE’s information. // Reanalysis: Used to show that new analysis occurred, but was not due to a modification from an external source.Analyzed CVEs do not show a banner on the vulnerability detail page. // // published (CVE list): The CVE Entry is populated with details. These are a CVE Description and reference link[s] regarding details of the CVE. // return db.VulnerabilityActive } return db.UnknownVulnerabilityStatus } func getAffected(cfg Config, vulnerability unmarshal.NVDVulnerability) []db.AffectedCPEHandle { candidates, err := allCandidates(vulnerability.ID, vulnerability.Configurations, cfg) if err != nil { log.WithFields("error", err).Warn("failed to process affected NVD CPEs") return nil } var affs []db.AffectedCPEHandle for _, candidate := range candidates { affs = append(affs, affectedApplicationPackage(cfg, vulnerability, candidate)...) } return affs } func getCWEs(vulnerability unmarshal.NVDVulnerability) []db.CWEHandle { var cwes []db.CWEHandle for _, w := range vulnerability.Weaknesses { for _, d := range w.Description { if !isValidCWE(d.Value) { continue } cwes = append(cwes, db.CWEHandle{ CVE: vulnerability.ID, CWE: d.Value, Source: w.Source, Type: w.Type, }) } } return cwes } func isValidCWE(cwe string) bool { switch cwe { case "": return false case "NVD-CWE-noinfo": return false // explicitly skip these rather than fill the database with meaningless entries case "NVD-CWE-Other": return true default: return cwePattern.MatchString(cwe) } } func encodeCPEs(cpes []cpe.Attributes) []string { var results []string for _, c := range cpes { results = append(results, c.String()) } return results } func affectedApplicationPackage(cfg Config, vulnerability unmarshal.NVDVulnerability, p affectedPackageCandidate) []db.AffectedCPEHandle { var affs []db.AffectedCPEHandle var qualifiers *db.PackageQualifiers if len(p.PlatformCPEs) > 0 { qualifiers = &db.PackageQualifiers{ PlatformCPEs: encodeCPEs(p.PlatformCPEs), } } affs = append(affs, db.AffectedCPEHandle{ CPE: getCPEFromAttributes(p.VulnerableCPE), BlobValue: &db.PackageBlob{ CVEs: []string{vulnerability.ID}, Qualifiers: qualifiers, Ranges: getRanges(cfg, p.VulnerableCPE, p.Ranges.toSlice(), vulnerability.ID), }, }) return affs } func getRanges(cfg Config, c cpe.Attributes, ras []affectedCPERange, vulnID string) []db.Range { var ranges []db.Range for _, ra := range ras { r := getRange(cfg, c, ra, vulnID) if r != nil { ranges = append(ranges, *r) } } return ranges } func getRange(cfg Config, c cpe.Attributes, ra affectedCPERange, vulnID string) *db.Range { return &db.Range{ Version: db.Version{ Type: getVersionFormat(c.Product), Constraint: ra.String(), }, Fix: getFix(cfg, c, ra, vulnID), } } func getFix(cfg Config, vulnCPE cpe.Attributes, ra affectedCPERange, vulnID string) *db.Fix { if !cfg.InferNVDFixVersions { return nil } possiblyFixed := strset.New() knownAffected := strset.New() unspecifiedSet := strset.New("*", "-", "*") if ra.VersionEndExcluding != "" && !unspecifiedSet.Has(ra.VersionEndExcluding) { possiblyFixed.Add(ra.VersionEndExcluding) } if ra.VersionStartIncluding != "" && !unspecifiedSet.Has(ra.VersionStartIncluding) { knownAffected.Add(ra.VersionStartIncluding) } if ra.VersionEndIncluding != "" && !unspecifiedSet.Has(ra.VersionEndIncluding) { knownAffected.Add(ra.VersionEndIncluding) } if !unspecifiedSet.Has(vulnCPE.Version) { knownAffected.Add(vulnCPE.Version) } possiblyFixed.Remove(knownAffected.List()...) if possiblyFixed.Size() != 1 { return nil } fixVersion := possiblyFixed.List()[0] // only include fix details if we have a date and kind that matches the inferred fix version var detail *db.FixDetail if ra.FixInfo != nil { if fixVersion == ra.FixInfo.Version { detail = &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime(ra.FixInfo.Date), Kind: ra.FixInfo.Kind, }, } } else { log.WithFields("cpe", vulnCPE, "vuln", vulnID, "range", ra, "fix", ra.FixInfo.Version).Debug("skipping fix detail because it does not match inferred fix version") } } return &db.Fix{ Version: fixVersion, State: db.FixedStatus, Detail: detail, } } func getCPEFromAttributes(atts cpe.Attributes) *db.Cpe { return &db.Cpe{ Part: atts.Part, Vendor: atts.Vendor, Product: atts.Product, Edition: atts.Edition, Language: atts.Language, SoftwareEdition: atts.SWEdition, TargetHardware: atts.TargetHW, TargetSoftware: atts.TargetSW, Other: atts.Other, } } func getSeverities(vuln unmarshal.NVDVulnerability) []db.Severity { sevs := nvd.CvssSummaries(vuln.CVSS()).Sorted() var results []db.Severity for _, sev := range sevs { priority := 2 if sev.Type == nvd.Primary { priority = 1 } results = append(results, db.Severity{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: sev.Vector, Version: sev.Version, }, Source: sev.Source, Rank: priority, }) } return results } func getReferences(vuln unmarshal.NVDVulnerability) []db.Reference { references := []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/" + vuln.ID, }, } for _, reference := range vuln.References { if reference.URL == "" { continue } tags := db.NormalizeReferenceTags(reference.Tags) sort.Strings(tags) // TODO there is other info we could be capturing too (source) references = append(references, db.Reference{ URL: reference.URL, Tags: tags, }) } return transformers.DeduplicateReferences(references) } ================================================ FILE: grype/db/v6/build/transformers/nvd/transform_test.go ================================================ package nvd import ( "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" ) var ( timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) listing = provider.File{ Path: "some", Digest: "123456", Algorithm: "sha256", } ) func inputProviderState(name string) provider.State { return provider.State{ Provider: name, Version: 12, Processor: "vunnel@1.2.3", Timestamp: timeVal, Listing: &listing, } } func expectedProvider(name string) *db.Provider { return &db.Provider{ ID: name, Version: "12", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", } } func TestTransform(t *testing.T) { tests := []struct { name string fixture string config Config provider string want []transformers.RelatedEntries }{ { name: "basic version range", fixture: "testdata/version-range.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-5487", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2018, 7, 5, 13, 52, 30, 627000000, time.UTC)), PublishedDate: timeRef(time.Date(2018, 5, 24, 14, 29, 0, 390000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-5487", Assigners: []string{"security-alert@netapp.com"}, Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", }, { URL: "https://security.netapp.com/advisory/ntap-20180523-0001/", Tags: []string{"patch", "vendor-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", Version: "4.0", }, Source: "security@zabbix.com", Rank: 2, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-5487"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*"}, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: ">= 7.2, <= 7.3", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "netapp", Product: "oncommand_unified_manager", }, }, db.CWEHandle{ CVE: "CVE-2018-5487", CWE: "CWE-20", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "with fix version information", fixture: "testdata/fix-version.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-5487", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2018, 7, 5, 13, 52, 30, 627000000, time.UTC)), PublishedDate: timeRef(time.Date(2018, 5, 24, 14, 29, 0, 390000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-5487", Assigners: []string{"security-alert@netapp.com"}, Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", }, { URL: "https://security.netapp.com/advisory/ntap-20180523-0001/", Tags: []string{"patch", "vendor-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", Version: "4.0", }, Source: "security@zabbix.com", Rank: 2, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-5487"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*"}, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: ">= 7.2, < 7.3", }, Fix: &db.Fix{ Version: "7.3", State: db.FixedStatus, Detail: &db.FixDetail{ // important! fix detail is associated to the record Available: &db.FixAvailability{ Date: timeRef(time.Date(2018, 5, 23, 0, 0, 0, 0, time.UTC)), Kind: "advisory", }, }, }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "netapp", Product: "oncommand_unified_manager", }, }, db.CWEHandle{ CVE: "CVE-2018-5487", CWE: "CWE-20", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "mismatched fix info", fixture: "testdata/fix-wrong-version.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-5487", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2018, 7, 5, 13, 52, 30, 627000000, time.UTC)), PublishedDate: timeRef(time.Date(2018, 5, 24, 14, 29, 0, 390000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-5487", Assigners: []string{"security-alert@netapp.com"}, Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", }, { URL: "https://security.netapp.com/advisory/ntap-20180523-0001/", Tags: []string{"patch", "vendor-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", Version: "4.0", }, Source: "security@zabbix.com", Rank: 2, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-5487"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*"}, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: ">= 7.2, < 7.3", }, Fix: &db.Fix{ Version: "7.3", State: db.FixedStatus, Detail: nil, // important! though there is fix info on the record, the versions mismatch, thus the detail is not attached (there is a bug upstream) }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "netapp", Product: "oncommand_unified_manager", }, }, db.CWEHandle{ CVE: "CVE-2018-5487", CWE: "CWE-20", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "single package, multiple distros", fixture: "testdata/single-package-multi-distro.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-1000222", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2020, 3, 31, 2, 15, 12, 667000000, time.UTC)), PublishedDate: timeRef(time.Date(2018, 8, 20, 20, 29, 1, 347000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-1000222", Assigners: []string{"cve@mitre.org"}, Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000222", }, { URL: "https://github.com/libgd/libgd/issues/447", Tags: []string{"issue-tracking", "third-party-advisory"}, }, { URL: "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", Tags: []string{"mailing-list", "third-party-advisory"}, }, { URL: "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", }, { URL: "https://security.gentoo.org/glsa/201903-18", Tags: []string{"third-party-advisory"}, }, { URL: "https://usn.ubuntu.com/3755-1/", Tags: []string{"mitigation", "third-party-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( // the application package... db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-1000222"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 2.2.5", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "libgd", Product: "libgd", }, }, // ubuntu OS ... (since the default config has all parts enabled, we should see this) db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-1000222"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 14.04", }, }, { Version: db.Version{ Constraint: "= 16.04", }, }, { Version: db.Version{ Constraint: "= 18.04", }, }, }, }, CPE: &db.Cpe{ Part: "o", Vendor: "canonical", Product: "ubuntu_linux", SoftwareEdition: "lts", }, }, // debian OS ... (since the default config has all parts enabled, we should see this) db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-1000222"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 8.0", }, }, }, }, CPE: &db.Cpe{ Part: "o", Vendor: "debian", Product: "debian_linux", }, }, db.CWEHandle{ CVE: "CVE-2018-1000222", CWE: "CWE-415", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "single package, multiple distros (application types only)", fixture: "testdata/single-package-multi-distro.json", provider: "nvd", config: func() Config { c := defaultConfig() c.CPEParts.Remove("h", "o") // important! return c }(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-1000222", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2020, 3, 31, 2, 15, 12, 667000000, time.UTC)), PublishedDate: timeRef(time.Date(2018, 8, 20, 20, 29, 1, 347000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-1000222", Assigners: []string{"cve@mitre.org"}, Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000222", }, { URL: "https://github.com/libgd/libgd/issues/447", Tags: []string{"issue-tracking", "third-party-advisory"}, }, { URL: "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", Tags: []string{"mailing-list", "third-party-advisory"}, }, { URL: "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", }, { URL: "https://security.gentoo.org/glsa/201903-18", Tags: []string{"third-party-advisory"}, }, { URL: "https://usn.ubuntu.com/3755-1/", Tags: []string{"mitigation", "third-party-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-1000222"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 2.2.5", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "libgd", Product: "libgd", }, }, db.CWEHandle{ CVE: "CVE-2018-1000222", CWE: "CWE-415", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "multiple packages, multiple distros", fixture: "testdata/compound-pkg.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-10189", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2018, 5, 23, 14, 41, 49, 73000000, time.UTC)), PublishedDate: timeRef(time.Date(2018, 4, 17, 20, 29, 0, 410000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-10189", Assigners: []string{"cve@mitre.org"}, Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2018-10189", }, { URL: "https://github.com/mautic/mautic/releases/tag/2.13.0", Tags: []string{"third-party-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-10189"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: ">= 1.0.0, <= 1.4.1", }, // since the top range operator is <= we cannot infer a fix }, { Version: db.Version{ Constraint: ">= 2.0.0, < 2.13.0", }, Fix: &db.Fix{ Version: "2.13.0", State: db.FixedStatus, }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "mautic", Product: "mautic", }, }, db.CWEHandle{ CVE: "CVE-2018-10189", CWE: "CWE-200", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "invalid CPE", fixture: "testdata/invalid_cpe.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2015-8978", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2016, 11, 28, 19, 50, 59, 600000000, time.UTC)), PublishedDate: timeRef(time.Date(2016, 11, 22, 17, 59, 0, 180000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2015-8978", Assigners: []string{"cve@mitre.org"}, Description: "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2015-8978", }, { URL: "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", Tags: []string{"vendor-advisory"}, }, { URL: "http://www.securityfocus.com/bid/94487", Tags: nil, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", Version: "3.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.CWEHandle{ CVE: "CVE-2015-8978", CWE: "CWE-399", Source: "nvd@nist.gov", Type: "Primary", }, ), // when we can't parse the CPE we should not add any affected CPE blobs (but we do add the vuln blob and CWE) }, }, }, { name: "basic platform CPE", fixture: "testdata/platform-cpe.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2022-26488", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2022, 9, 3, 3, 34, 19, 933000000, time.UTC)), PublishedDate: timeRef(time.Date(2022, 3, 10, 17, 47, 45, 383000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2022-26488", Assigners: []string{"cve@mitre.org"}, Description: "In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2022-26488", }, { URL: "https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/", Tags: []string{"patch", "vendor-advisory"}, }, { URL: "https://security.netapp.com/advisory/ntap-20220419-0005/", Tags: []string{"third-party-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:L/AC:M/Au:N/C:P/I:P/A:P", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2022-26488"}, Ranges: []db.Range{ { // match all versions Version: db.Version{Constraint: ""}, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "netapp", Product: "active_iq_unified_manager", TargetSoftware: "windows", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2022-26488"}, Ranges: []db.Range{ { // match all versions Version: db.Version{Constraint: ""}, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "netapp", Product: "ontap_select_deploy_administration_utility", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2022-26488"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"}, // important! }, Ranges: []db.Range{ {Version: db.Version{Constraint: "<= 3.7.12"}}, {Version: db.Version{Constraint: ">= 3.10.0, <= 3.10.2"}}, {Version: db.Version{Constraint: ">= 3.8.0, <= 3.8.12"}}, {Version: db.Version{Constraint: ">= 3.9.0, <= 3.9.10"}}, {Version: db.Version{Constraint: "= 3.11.0-alpha1"}}, {Version: db.Version{Constraint: "= 3.11.0-alpha2"}}, {Version: db.Version{Constraint: "= 3.11.0-alpha3"}}, {Version: db.Version{Constraint: "= 3.11.0-alpha4"}}, {Version: db.Version{Constraint: "= 3.11.0-alpha5"}}, {Version: db.Version{Constraint: "= 3.11.0-alpha6"}}, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "python", Product: "python", }, }, db.CWEHandle{ CVE: "CVE-2022-26488", CWE: "CWE-426", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "multiple platform CPEs for single package", fixture: "testdata/cve-2022-0543.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2022-0543", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2023, 9, 29, 15, 55, 24, 533000000, time.UTC)), PublishedDate: timeRef(time.Date(2022, 2, 18, 20, 15, 17, 583000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2022-0543", Assigners: []string{"security@debian.org"}, Description: "It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2022-0543", }, { URL: "http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html", Tags: []string{"exploit", "third-party-advisory", "vdb-entry"}, }, { URL: "https://bugs.debian.org/1005787", Tags: []string{"issue-tracking", "patch", "third-party-advisory"}, }, { URL: "https://lists.debian.org/debian-security-announce/2022/msg00048.html", Tags: []string{"mailing-list", "third-party-advisory"}, }, { URL: "https://security.netapp.com/advisory/ntap-20220331-0004/", Tags: []string{"third-party-advisory"}, }, { URL: "https://www.debian.org/security/2022/dsa-5081", Tags: []string{"mailing-list", "third-party-advisory"}, }, { URL: "https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce", Tags: []string{"third-party-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:N/AC:L/Au:N/C:C/I:C/A:C", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2022-0543"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{ "cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*", "cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*", "cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*", "cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*", "cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*", }, }, Ranges: []db.Range{ { // match all versions Version: db.Version{Constraint: ""}, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "redis", Product: "redis", }, }, db.CWEHandle{ CVE: "CVE-2022-0543", CWE: "CWE-862", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "multiple platform CPEs for single package + fix and OS match", fixture: "testdata/cve-2020-10729.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2020-10729", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2021, 12, 10, 19, 57, 6, 357000000, time.UTC)), PublishedDate: timeRef(time.Date(2021, 5, 27, 19, 15, 7, 880000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2020-10729", Assigners: []string{"secalert@redhat.com"}, Description: "A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2020-10729", }, { URL: "https://bugzilla.redhat.com/show_bug.cgi?id=1831089", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://github.com/ansible/ansible/issues/34144", Tags: []string{"exploit", "issue-tracking", "third-party-advisory"}, }, { URL: "https://www.debian.org/security/2021/dsa-4950", Tags: []string{"third-party-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "AV:L/AC:L/Au:N/C:P/I:N/A:N", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-10729"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{ "cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*", "cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*", }, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: "< 2.9.6", }, Fix: &db.Fix{ Version: "2.9.6", State: db.FixedStatus, }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "redhat", Product: "ansible_engine", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-10729"}, // note: no qualifiers ! Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 10.0", }, // note: no fix! }, }, }, CPE: &db.Cpe{ Part: "o", Vendor: "debian", Product: "debian_linux", }, }, db.CWEHandle{ CVE: "CVE-2020-10729", CWE: "CWE-330", Source: "nvd@nist.gov", Type: "Primary", }, db.CWEHandle{ CVE: "CVE-2020-10729", CWE: "CWE-330", Source: "secalert@redhat.com", Type: "Secondary", }, ), }, }, }, { name: "application type as platform CPE", fixture: "testdata/multiple-platforms-with-application-cpe.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-38733", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2023, 8, 26, 2, 25, 42, 957000000, time.UTC)), PublishedDate: timeRef(time.Date(2023, 8, 22, 22, 15, 8, 460000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-38733", Assigners: []string{"psirt@us.ibm.com"}, Description: "IBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs. IBM X-Force Id: 262293.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-38733", }, { URL: "https://exchange.xforce.ibmcloud.com/vulnerabilities/262293", Tags: []string{"vdb-entry", "vendor-advisory"}, }, { URL: "https://www.ibm.com/support/pages/node/7028223", Tags: []string{"patch", "vendor-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", Version: "3.1", }, Source: "psirt@us.ibm.com", Rank: 2, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-38733"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{ "cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*", "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*", }, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: ">= 21.0.0, <= 21.0.7.3", }, }, { Version: db.Version{ Constraint: ">= 23.0.0, <= 23.0.3", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "ibm", Product: "robotic_process_automation", }, }, db.CWEHandle{ CVE: "CVE-2023-38733", CWE: "CWE-532", Source: "nvd@nist.gov", Type: "Primary", }, db.CWEHandle{ CVE: "CVE-2023-38733", CWE: "CWE-532", Source: "psirt@us.ibm.com", Type: "Secondary", }, ), }, }, }, { name: "can process entries when the platform CPE is first", fixture: "testdata/CVE-2023-45283-platform-cpe-first.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-45283", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2023, 12, 14, 10, 15, 7, 947000000, time.UTC)), PublishedDate: timeRef(time.Date(2023, 11, 9, 17, 15, 8, 757000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-45283", Assigners: []string{"security@golang.org"}, Description: "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-45283", }, { URL: "http://www.openwall.com/lists/oss-security/2023/12/05/2", Tags: nil, }, { URL: "https://go.dev/cl/540277", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://go.dev/cl/541175", Tags: nil, }, { URL: "https://go.dev/issue/63713", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://go.dev/issue/64028", Tags: nil, }, { URL: "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", Tags: []string{"issue-tracking", "mailing-list", "vendor-advisory"}, }, { URL: "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", Tags: nil, }, { URL: "https://pkg.go.dev/vuln/GO-2023-2185", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://security.netapp.com/advisory/ntap-20231214-0008/", Tags: nil, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-45283"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"}, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: "< 1.20.11", }, Fix: &db.Fix{ Version: "1.20.11", State: db.FixedStatus, }, }, { Version: db.Version{ Constraint: ">= 1.21.0-0, < 1.21.4", }, Fix: &db.Fix{ Version: "1.21.4", State: db.FixedStatus, }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "golang", Product: "go", }, }, db.CWEHandle{ CVE: "CVE-2023-45283", CWE: "CWE-22", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "can process entries when the platform CPE is last", fixture: "testdata/CVE-2023-45283-platform-cpe-last.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-45283", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2023, 12, 14, 10, 15, 7, 947000000, time.UTC)), PublishedDate: timeRef(time.Date(2023, 11, 9, 17, 15, 8, 757000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-45283", Assigners: []string{"security@golang.org"}, Description: "The filepath package does not recognize paths with a \\??\\ prefix as special. On Windows, a path beginning with \\??\\ is a Root Local Device path equivalent to a path beginning with \\\\?\\. Paths with a \\??\\ prefix may be used to access arbitrary locations on the system. For example, the path \\??\\c:\\x is equivalent to the more common path c:\\x. Before fix, Clean could convert a rooted path such as \\a\\..\\??\\b into the root local device path \\??\\b. Clean will now convert this to .\\??\\b. Similarly, Join(\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\??\\b. Join will now convert this to \\.\\??\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\??\\ as absolute, and VolumeName correctly reports the \\??\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\?, resulting in filepath.Clean(\\?\\c:) returning \\?\\c: rather than \\?\\c:\\ (among other effects). The previous behavior has been restored.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-45283", }, { URL: "http://www.openwall.com/lists/oss-security/2023/12/05/2", Tags: nil, }, { URL: "https://go.dev/cl/540277", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://go.dev/cl/541175", Tags: nil, }, { URL: "https://go.dev/issue/63713", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://go.dev/issue/64028", Tags: nil, }, { URL: "https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY", Tags: []string{"issue-tracking", "mailing-list", "vendor-advisory"}, }, { URL: "https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ", Tags: nil, }, { URL: "https://pkg.go.dev/vuln/GO-2023-2185", Tags: []string{"issue-tracking", "vendor-advisory"}, }, { URL: "https://security.netapp.com/advisory/ntap-20231214-0008/", Tags: nil, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-45283"}, Qualifiers: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"}, }, Ranges: []db.Range{ { Version: db.Version{ Constraint: "< 1.20.11", }, Fix: &db.Fix{ Version: "1.20.11", State: db.FixedStatus, }, }, { Version: db.Version{ Constraint: ">= 1.21.0-0, < 1.21.4", }, Fix: &db.Fix{ Version: "1.21.4", State: db.FixedStatus, }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "golang", Product: "go", }, }, db.CWEHandle{ CVE: "CVE-2023-45283", CWE: "CWE-22", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "a simple list of OS matches", // note: this was modified relative to the upstream data to account for additional interesting cases fixture: "testdata/cve-2024-26663-standalone-os.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2024-26663", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2025, 1, 7, 17, 20, 30, 367000000, time.UTC)), PublishedDate: timeRef(time.Date(2024, 4, 2, 7, 15, 43, 287000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2024-26663", Assigners: []string{"416baaa9-dc9f-4396-8d5f-8c081fb06d67"}, Description: "the description...", References: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2024-26663"}, { URL: "https://git.kernel.org/stable/c/0cd331dfd6023640c9669d0592bc0fd491205f87", Tags: []string{"patch"}, }, }, Severities: []db.Severity{ { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2024-26663"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 10.0", }, }, }, }, CPE: &db.Cpe{ Part: "o", Vendor: "debian", Product: "debian_linux", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2024-26663"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: ">= 4.9, < 4.19.307", }, Fix: &db.Fix{ State: db.FixedStatus, Version: "4.19.307", }, }, { Version: db.Version{ Constraint: ">= 6.7, < 6.7.5", }, Fix: &db.Fix{ State: db.FixedStatus, Version: "6.7.5", }, }, { Version: db.Version{ Constraint: "= 6.8-rc1", }, }, { Version: db.Version{ Constraint: "= 6.8-rc2", }, }, { Version: db.Version{ Constraint: "= 6.8-rc3", }, }, }, }, CPE: &db.Cpe{ Part: "o", Vendor: "linux", Product: "linux_kernel", }, }, db.CWEHandle{ CVE: "CVE-2024-26663", CWE: "CWE-476", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "drops nodes with unsupported topology", fixture: "testdata/cve-2021-1566.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2021-1566", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2024, 11, 21, 5, 44, 38, 237000000, time.UTC)), PublishedDate: timeRef(time.Date(2021, 6, 16, 18, 15, 8, 710000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2021-1566", Assigners: []string{"psirt@cisco.com"}, Description: "description.", References: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2021-1566"}, { URL: "https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-esa-wsa-cert-vali-n8L97RW", Tags: []string{"vendor-advisory"}, }, }, Severities: []db.Severity{ { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:N", Version: "2.0", }, Source: "nvd@nist.gov", Rank: 1, }, { Scheme: "CVSS", Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", Version: "3.1", }, Source: "psirt@cisco.com", Rank: 2, }, }, }, }, Related: relatedEntries( db.CWEHandle{ CVE: "CVE-2021-1566", CWE: "CWE-296", Source: "psirt@cisco.com", Type: "Secondary", }, db.CWEHandle{ CVE: "CVE-2021-1566", CWE: "CWE-295", Source: "nvd@nist.gov", Type: "Primary", }, ), // important! we dropped all of the node criteria since the topology is unsupported }, }, }, { name: "considers non-standard CPE fields", fixture: "testdata/CVE-2008-3442.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2008-3442", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2008, 9, 5, 21, 43, 5, 500000000, time.UTC)), PublishedDate: timeRef(time.Date(2008, 8, 1, 14, 41, 0, 0, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2008-3442", Assigners: []string{"cve@mitre.org"}, Description: "desc.", References: []db.Reference{{URL: "https://nvd.nist.gov/vuln/detail/CVE-2008-3442"}}, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2008-3442"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 10.0", }, }, { Version: db.Version{ Constraint: "= 7.0", }, }, { Version: db.Version{ Constraint: "= 8.0", }, }, { Version: db.Version{ Constraint: "= 8.1", }, }, { Version: db.Version{ Constraint: "= 9.0", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "winzip", Product: "winzip", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2008-3442"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "= 8.1", }, }, { Version: db.Version{ Constraint: "= 9.0", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "winzip", Product: "winzip", Edition: "sr1", }, }, db.CWEHandle{ CVE: "CVE-2008-3442", CWE: "CWE-94", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "https://github.com/anchore/grype/issues/2807#issuecomment-3101447594", fixture: "testdata/CVE-2004-0377.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2004-0377", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2025, 4, 3, 1, 3, 51, 193000000, time.UTC)), PublishedDate: timeRef(time.Date(2004, 5, 4, 4, 0, 0, 0, time.UTC)), Status: db.UnknownVulnerabilityStatus, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2004-0377", Assigners: []string{"cve@mitre.org"}, Description: "Buffer overflow in the win32_stat function for (1) ActiveState's ActivePerl and (2) Larry Wall's Perl before 5.8.3 allows local or remote attackers to execute arbitrary commands via filenames that end in a backslash character.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2004-0377", }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2004-0377"}, Ranges: nil, }, CPE: &db.Cpe{ Part: "a", Vendor: "activestate", Product: "activeperl", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2004-0377"}, Ranges: []db.Range{ { Version: db.Version{ Constraint: "<= 5.8.3", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "larry_wall", Product: "perl", }, }, db.CWEHandle{ CVE: "CVE-2004-0377", CWE: "NVD-CWE-Other", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, { name: "JVM packages version format detection", fixture: "testdata/jvm-packages.json", provider: "nvd", config: defaultConfig(), want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-JVM-TEST", ProviderID: "nvd", Provider: expectedProvider("nvd"), ModifiedDate: timeRef(time.Date(2024, 1, 23, 16, 32, 52, 103000000, time.UTC)), PublishedDate: timeRef(time.Date(2024, 1, 17, 0, 15, 51, 677000000, time.UTC)), Status: db.VulnerabilityActive, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-JVM-TEST", Assigners: []string{"cve@mitre.org"}, Description: "Test vulnerability affecting JVM packages to demonstrate version format detection.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-JVM-TEST", }, { URL: "https://www.oracle.com/security-alerts/cpujan2024.html", Tags: []string{"patch", "vendor-advisory"}, }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", Version: "3.1", }, Source: "nvd@nist.gov", Rank: 1, }, }, }, }, Related: relatedEntries( db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-JVM-TEST"}, Ranges: []db.Range{ { Version: db.Version{ Type: "jvm", Constraint: "= 17.0.10", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "adoptium", Product: "java", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-JVM-TEST"}, Ranges: []db.Range{ { Version: db.Version{ Type: "jvm", Constraint: "= 21.0.2", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "azul", Product: "zulu", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-JVM-TEST"}, Ranges: []db.Range{ { Version: db.Version{ Type: "jvm", Constraint: "= 17.0.10", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "eclipse", Product: "openjdk", }, }, db.AffectedCPEHandle{ BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2023-JVM-TEST"}, Ranges: []db.Range{ { Version: db.Version{ Type: "jvm", Constraint: "= 11.0.22", }, }, { Version: db.Version{ Type: "jvm", Constraint: "= 8u401", }, }, }, }, CPE: &db.Cpe{ Part: "a", Vendor: "oracle", Product: "jdk", }, }, db.CWEHandle{ CVE: "CVE-2023-JVM-TEST", CWE: "CWE-79", Source: "nvd@nist.gov", Type: "Primary", }, ), }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { vulns := loadFixture(t, test.fixture) var actual []transformers.RelatedEntries for _, vuln := range vulns { if test.config == (Config{}) { test.config = defaultConfig() } entries, err := Transformer(test.config)(vuln, inputProviderState(test.provider)) require.NoError(t, err) for _, entry := range entries { e, ok := entry.Data.(transformers.RelatedEntries) require.True(t, ok) actual = append(actual, e) } } if diff := cmp.Diff(test.want, actual); diff != "" { t.Errorf("data entries mismatch (-want +got):\n%s", diff) } }) } } func relatedEntries(items ...any) []any { return items } func loadFixture(t *testing.T, fixturePath string) []unmarshal.NVDVulnerability { t.Helper() f, err := os.Open(fixturePath) require.NoError(t, err) defer func() { require.NoError(t, f.Close()) }() entries, err := unmarshal.NvdVulnerabilityEntries(f) require.NoError(t, err) var vulns []unmarshal.NVDVulnerability for _, entry := range entries { vulns = append(vulns, entry.Cve) } return vulns } func timeRef(ti time.Time) *time.Time { return &ti } func TestIsValidCWE(t *testing.T) { tests := []struct { name string cwe string expected bool }{ { name: "empty string", cwe: "", expected: false, }, { name: "NVD-CWE-noinfo", cwe: "NVD-CWE-noinfo", expected: false, }, { name: "NVD-CWE-Other", cwe: "NVD-CWE-Other", expected: true, }, { name: "valid CWE with single digit", cwe: "CWE-1", expected: true, }, { name: "valid CWE with multiple digits", cwe: "CWE-123", expected: true, }, { name: "valid CWE-79", cwe: "CWE-79", expected: true, }, { name: "valid CWE-89", cwe: "CWE-89", expected: true, }, { name: "invalid CWE without prefix", cwe: "123", expected: false, }, { name: "invalid CWE without number", cwe: "CWE-", expected: false, }, { name: "invalid CWE with letters", cwe: "CWE-ABC", expected: false, }, { name: "invalid lowercase cwe", cwe: "cwe-123", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isValidCWE(tt.cwe) if got != tt.expected { t.Errorf("isValidCWE(%q) = %v, want %v", tt.cwe, got, tt.expected) } }) } } func TestGetReferences(t *testing.T) { tests := []struct { name string vuln unmarshal.NVDVulnerability expected []db.Reference }{ { name: "no upstream references - only canonical NVD URL", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{}, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, }, }, { name: "single unique reference", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com/advisory", Tags: []string{"patch", "vendor-advisory"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com/advisory", Tags: []string{"patch", "vendor-advisory"}}, }, }, { name: "multiple unique references", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com/advisory", Tags: []string{"patch"}}, {URL: "https://github.com/project/issues/123", Tags: []string{"issue-tracking"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com/advisory", Tags: []string{"patch"}}, {URL: "https://github.com/project/issues/123", Tags: []string{"issue-tracking"}}, }, }, { name: "exact duplicate references", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com", Tags: []string{"patch", "vendor-advisory"}}, {URL: "https://example.com", Tags: []string{"patch", "vendor-advisory"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com", Tags: []string{"patch", "vendor-advisory"}}, }, }, { name: "duplicate with tags in different order (congruent sets)", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com", Tags: []string{"patch", "vendor-advisory"}}, {URL: "https://example.com", Tags: []string{"vendor-advisory", "patch"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com", Tags: []string{"patch", "vendor-advisory"}}, }, }, { name: "same URL with different tags - keep both", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com", Tags: []string{"patch"}}, {URL: "https://example.com", Tags: []string{"vendor-advisory"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com", Tags: []string{"patch"}}, {URL: "https://example.com", Tags: []string{"vendor-advisory"}}, }, }, { name: "same URL with and without tags - keep both", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com", Tags: []string{"patch"}}, {URL: "https://example.com", Tags: nil}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com", Tags: []string{"patch"}}, {URL: "https://example.com"}, }, }, { name: "duplicate canonical NVD URL in upstream data", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345", Tags: nil}, {URL: "https://example.com/advisory", Tags: []string{"vendor-advisory"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com/advisory", Tags: []string{"vendor-advisory"}}, }, }, { name: "empty URL in upstream data - should be filtered", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "", Tags: []string{"patch"}}, {URL: "https://example.com", Tags: []string{"vendor-advisory"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com", Tags: []string{"vendor-advisory"}}, }, }, { name: "multiple duplicates among many references", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com/1", Tags: []string{"patch"}}, {URL: "https://example.com/2", Tags: []string{"vendor-advisory"}}, {URL: "https://example.com/1", Tags: []string{"patch"}}, {URL: "https://example.com/3", Tags: nil}, {URL: "https://example.com/2", Tags: []string{"vendor-advisory"}}, {URL: "https://example.com/3", Tags: []string{}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com/1", Tags: []string{"patch"}}, {URL: "https://example.com/2", Tags: []string{"vendor-advisory"}}, {URL: "https://example.com/3"}, }, }, { name: "preserves order of first occurrence", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com/1", Tags: []string{"patch"}}, {URL: "https://example.com/2", Tags: []string{"advisory"}}, {URL: "https://example.com/3", Tags: []string{"exploit"}}, {URL: "https://example.com/1", Tags: []string{"patch"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com/1", Tags: []string{"patch"}}, {URL: "https://example.com/2", Tags: []string{"advisory"}}, {URL: "https://example.com/3", Tags: []string{"exploit"}}, }, }, { name: "complex real-world scenario with duplicates and tag reordering", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://github.com/project/issues/123", Tags: []string{"issue-tracking", "third-party-advisory"}}, {URL: "https://security.vendor.com/advisory", Tags: []string{"patch", "vendor-advisory"}}, {URL: "https://github.com/project/issues/123", Tags: []string{"third-party-advisory", "issue-tracking"}}, {URL: "https://lists.vendor.com/announce", Tags: []string{"mailing-list"}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://github.com/project/issues/123", Tags: []string{"issue-tracking", "third-party-advisory"}}, {URL: "https://security.vendor.com/advisory", Tags: []string{"patch", "vendor-advisory"}}, {URL: "https://lists.vendor.com/announce", Tags: []string{"mailing-list"}}, }, }, { name: "references with nil tags vs empty tags - treated as identical", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com", Tags: nil}, {URL: "https://example.com", Tags: []string{}}, }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com"}, }, }, { name: "duplicate after different tags on same URL - exposes logic bug", vuln: unmarshal.NVDVulnerability{ ID: "CVE-2023-12345", References: []nvd.Reference{ {URL: "https://example.com", Tags: []string{"patch"}}, {URL: "https://example.com", Tags: []string{"vendor-advisory"}}, {URL: "https://example.com", Tags: []string{"patch"}}, // duplicate of first }, }, expected: []db.Reference{ {URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"}, {URL: "https://example.com", Tags: []string{"patch"}}, {URL: "https://example.com", Tags: []string{"vendor-advisory"}}, // Should NOT have a duplicate "patch" entry here }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := getReferences(tt.vuln) if diff := cmp.Diff(tt.expected, actual, cmpopts.EquateEmpty()); diff != "" { t.Errorf("getReferences() mismatch (-want +got):\n%s", diff) } }) } } ================================================ FILE: grype/db/v6/build/transformers/openvex/transform.go ================================================ package openvex import ( "fmt" "sort" govex "github.com/openvex/go-vex/pkg/vex" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/version" "github.com/anchore/packageurl-go" syftPkg "github.com/anchore/syft/syft/pkg" ) func AnnotatedTransform(wrapper unmarshal.AnnotatedOpenVEXVulnerability, state provider.State) ([]data.Entry, error) { return transform(wrapper.Document, state, wrapper.Fixes) } func Transform(vulnerability unmarshal.OpenVEXVulnerability, state provider.State) ([]data.Entry, error) { return transform(vulnerability, state, nil) } func transform(vulnerability unmarshal.OpenVEXVulnerability, state provider.State, fixes []unmarshal.AnnotatedOpenVEXFix) ([]data.Entry, error) { name := getName(&vulnerability) vulnHandle := db.VulnerabilityHandle{ Name: name, Status: db.VulnerabilityActive, PublishedDate: vulnerability.Timestamp, ModifiedDate: vulnerability.LastUpdated, ProviderID: state.Provider, Provider: provider.Model(state), BlobValue: &db.VulnerabilityBlob{ ID: name, Assigners: nil, Description: vulnerability.Vulnerability.Description, References: getReferences(&vulnerability), Aliases: getAliases(&vulnerability), }, } pkgs, err := getPackageHandles(&vulnerability, fixes) if err != nil { return nil, err } in := []any{vulnHandle} in = append(in, pkgs...) return transformers.NewEntries(in...), nil } // getPackageHandles for all products in this advisory func getPackageHandles(vuln *unmarshal.OpenVEXVulnerability, fixes []unmarshal.AnnotatedOpenVEXFix) ([]any, error) { if len(vuln.Products) == 0 { return nil, nil } fixesByProduct := make(map[string][]unmarshal.AnnotatedOpenVEXFix) for _, fix := range fixes { fixesByProduct[fix.Product] = append(fixesByProduct[fix.Product], fix) } var aphs []db.AffectedPackageHandle var uaphs []db.UnaffectedPackageHandle for _, product := range vuln.Products { aph, uph, err := getPackageHandle(&product, vuln, fixesByProduct[product.Identifiers[govex.PURL]]) if err != nil { return nil, err } aphs = append(aphs, aph...) uaphs = append(uaphs, uph...) } sort.Sort(internal.ByAffectedPackage(aphs)) sort.Sort(internal.ByUnaffectedPackage(uaphs)) var all []any for i := range aphs { all = append(all, aphs[i]) } for i := range uaphs { all = append(all, uaphs[i]) } return all, nil } // getPackageHandle for a single product // // OpenVEX defines product via: // // Component { // Identifiers: { // PURLIdentifierType: pkg:type/name@version // } // } func getPackageHandle(product *govex.Product, vuln *unmarshal.OpenVEXVulnerability, fixes []unmarshal.AnnotatedOpenVEXFix) (aphs []db.AffectedPackageHandle, uphs []db.UnaffectedPackageHandle, err error) { if product == nil || vuln == nil { return nil, nil, fmt.Errorf("getAffectedPackage params cannot be nil") } purl, err := getPURL(product) if err != nil { return nil, nil, fmt.Errorf("failed to parse purl %s: %w", purl, err) } pkg := &db.Package{ Ecosystem: string(syftPkg.TypeFromPURL(purl.String())), Name: purl.Name, } aliases := []string{getName(vuln)} aliases = append(aliases, getAliases(vuln)...) switch vuln.Status { case govex.StatusAffected: aphs = append(aphs, db.AffectedPackageHandle{ Package: pkg, BlobValue: getPackageBlob(aliases, purl.Version, purl.Type, "", fixes), }) case govex.StatusNotAffected: uphs = append(uphs, db.UnaffectedPackageHandle{ Package: pkg, BlobValue: getPackageBlob(aliases, purl.Version, purl.Type, db.NotAffectedFixStatus, fixes), }) case govex.StatusFixed: uphs = append(uphs, db.UnaffectedPackageHandle{ Package: pkg, BlobValue: getPackageBlob(aliases, purl.Version, purl.Type, db.FixedStatus, fixes), }) default: err = fmt.Errorf("invalid vuln states %s", vuln.Status) } return aphs, uphs, err } // getPURL from either ID field or identifiers func getPURL(product *govex.Product) (purl *packageurl.PackageURL, err error) { if p, ok := product.Identifiers[govex.PURL]; ok { purl, err := packageurl.FromString(p) if err != nil { return nil, fmt.Errorf("failed to parse purl %s: %w", p, err) } return &purl, nil } if product.ID != "" { purl, err := packageurl.FromString(product.ID) if err != nil { return nil, err } return &purl, nil } return nil, fmt.Errorf("invalid product: %v", product) } func getAliases(vuln *unmarshal.OpenVEXVulnerability) []string { ret := make([]string, 0, len(vuln.Vulnerability.Aliases)) for _, alias := range vuln.Vulnerability.Aliases { ret = append(ret, string(alias)) } return ret } func getName(vuln *unmarshal.OpenVEXVulnerability) string { return string(vuln.Vulnerability.Name) } func getReferences(vuln *unmarshal.OpenVEXVulnerability) []db.Reference { refs := []db.Reference{ { URL: getName(vuln), }, } return refs } func getPackageBlob(aliases []string, ver string, ty string, fixState db.FixStatus, fixes []unmarshal.AnnotatedOpenVEXFix) *db.PackageBlob { var fix *db.Fix if fixState != "" { fix = &db.Fix{ State: fixState, } canExpressFixVersion := ver != "" && fixState == db.FixedStatus if canExpressFixVersion { // only express a fix version if we have a version and the state is "fixed" fix.Version = ver } canExpressFixDetail := len(fixes) > 0 && canExpressFixVersion var detail *db.FixDetail if canExpressFixDetail { time := internal.ParseTime(fixes[0].Available.Date) if time != nil && !time.IsZero() { detail = &db.FixDetail{ Available: &db.FixAvailability{ Date: time, Kind: fixes[0].Available.Kind, }, } } } fix.Detail = detail } return &db.PackageBlob{ CVEs: aliases, Ranges: []db.Range{ { Version: db.Version{ Type: version.ParseFormat(ty).String(), Constraint: fmt.Sprintf("= %s", ver), }, Fix: fix, }, }, } } ================================================ FILE: grype/db/v6/build/transformers/openvex/transform_test.go ================================================ package openvex import ( "testing" "time" "github.com/google/go-cmp/cmp" govex "github.com/openvex/go-vex/pkg/vex" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/version" ) var timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) var listing = provider.File{ Path: "some", Digest: "123456", Algorithm: "sha256", } func inputProviderState() provider.State { return provider.State{ Provider: "openvex", Version: 1, Processor: "vunnel@1.2.3", Timestamp: timeVal, Listing: &listing, } } func TestOpenVEXTransform(t *testing.T) { tests := []struct { name string state provider.State vuln unmarshal.OpenVEXVulnerability wantErr bool want transformers.RelatedEntries }{ { name: "basic OpenVEX vulnerability with single product", state: inputProviderState(), vuln: govex.Statement{ ID: "test-transform-1", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@1.26.16", }, }, }, }, Status: govex.StatusAffected, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Description: "urllib3 HTTP Request Smuggling vulnerability", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, want: transformers.RelatedEntries{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "cve-2023-43804", Status: db.VulnerabilityActive, ProviderID: "openvex", Provider: &db.Provider{ ID: "openvex", Version: "1", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", }, BlobValue: &db.VulnerabilityBlob{ ID: "cve-2023-43804", Description: "urllib3 HTTP Request Smuggling vulnerability", References: []db.Reference{ { URL: "cve-2023-43804", }, }, Aliases: []string{"ghsa-v845-jxx5-vc9f"}, }, }, Related: []any{ db.AffectedPackageHandle{ Package: &db.Package{ // convert pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 1.26.16", }, }, }, }, }, }, }, }, { name: "OpenVEX vulnerability with multiple products", state: inputProviderState(), vuln: govex.Statement{ ID: "test-transform-2", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@1.26.16", }, }, }, { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:npm/express@4.18.2", }, }, }, }, Status: govex.StatusNotAffected, // important! this means the versions will not be attributed to fixes Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Description: "Test vulnerability affecting multiple packages", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, want: transformers.RelatedEntries{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "cve-2023-43804", Status: db.VulnerabilityActive, ProviderID: "openvex", Provider: &db.Provider{ ID: "openvex", Version: "1", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", }, BlobValue: &db.VulnerabilityBlob{ ID: "cve-2023-43804", Description: "Test vulnerability affecting multiple packages", References: []db.Reference{ { URL: "cve-2023-43804", }, }, Aliases: []string{"ghsa-v845-jxx5-vc9f"}, }, }, Related: []any{ db.UnaffectedPackageHandle{ Package: &db.Package{ Ecosystem: "npm", Name: "express", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.SemanticFormat.String(), Constraint: "= 4.18.2", }, Fix: &db.Fix{ State: db.NotAffectedFixStatus, }, }, }, }, }, db.UnaffectedPackageHandle{ Package: &db.Package{ // convert pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 1.26.16", }, Fix: &db.Fix{ State: db.NotAffectedFixStatus, }, }, }, }, }, }, }, }, { name: "OpenVEX vulnerability with no products", state: inputProviderState(), vuln: govex.Statement{ ID: "test-transform-3", Products: []govex.Product{}, Status: govex.StatusNotAffected, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Description: "Test vulnerability with no products", }, }, want: transformers.RelatedEntries{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "cve-2023-43804", Status: db.VulnerabilityActive, ProviderID: "openvex", Provider: &db.Provider{ ID: "openvex", Version: "1", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", }, BlobValue: &db.VulnerabilityBlob{ ID: "cve-2023-43804", Description: "Test vulnerability with no products", References: []db.Reference{ { URL: "cve-2023-43804", }, }, Aliases: []string{}, }, }, }, }, { name: "OpenVEX vulnerability with invalid product purl", state: inputProviderState(), vuln: govex.Statement{ ID: "test-transform-4", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "invalid-purl", }, }, }, }, Status: govex.StatusAffected, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Description: "Test vulnerability with invalid purl", }, }, wantErr: true, }, { name: "OpenVEX vulnerability with empty product", state: inputProviderState(), vuln: govex.Statement{ ID: "test-transform-4", Products: []govex.Product{{}}, Status: govex.StatusAffected, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Description: "Test vulnerability with invalid purl", }, }, wantErr: true, }, } t.Parallel() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Transform(tt.vuln, tt.state) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) require.Len(t, got, 1, "should return exactly one RelatedEntries") e, ok := got[0].Data.(transformers.RelatedEntries) require.True(t, ok) if diff := cmp.Diff(tt.want, e); diff != "" { t.Errorf("data entries mismatch (-want +got):\n%s", diff) } }) } } func Test_GetPackageHandles(t *testing.T) { tests := []struct { name string vuln unmarshal.OpenVEXVulnerability fixes []unmarshal.AnnotatedOpenVEXFix want []any wantErr bool }{ { name: "empty products returns nil", vuln: govex.Statement{ ID: "test-vuln-1", Products: []govex.Product{}, Status: govex.StatusAffected, }, want: nil, wantErr: false, }, { name: "single affected product", vuln: govex.Statement{ ID: "test-vuln-2", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@1.26.16", }, }, }, }, Status: govex.StatusAffected, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, want: []any{ db.AffectedPackageHandle{ Package: &db.Package{ // converts pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 1.26.16", }, }, }, }, }, }, wantErr: false, }, { name: "single product with fix", vuln: govex.Statement{ ID: "test-vuln-2", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@1.26.16", }, }, }, }, Status: govex.StatusFixed, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, fixes: []unmarshal.AnnotatedOpenVEXFix{ { Available: unmarshal.AnnotatedOpenVEXFixAvailability{ Date: "2025-01-01", Kind: "advisory", }, Product: "pkg:pypi/urllib3@1.26.16", }, }, want: []any{ db.UnaffectedPackageHandle{ Package: &db.Package{ // converts pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 1.26.16", }, Fix: &db.Fix{ Version: "1.26.16", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: internal.ParseTime("2025-01-01"), Kind: "advisory", }, }, }, }, }, }, }, }, wantErr: false, }, { name: "don't include fix detail for a mismatched status", vuln: govex.Statement{ ID: "test-vuln-2", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@1.26.16", }, }, }, }, Status: govex.StatusNotAffected, // important! not-affected != fixed Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, fixes: []unmarshal.AnnotatedOpenVEXFix{ // important! there is a fix, but the status is not "fixed", so this should never be associated with the package { Available: unmarshal.AnnotatedOpenVEXFixAvailability{ Date: "2025-01-01", Kind: "advisory", }, Product: "pkg:pypi/urllib3@1.26.16", }, }, want: []any{ db.UnaffectedPackageHandle{ Package: &db.Package{ // converts pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 1.26.16", }, Fix: &db.Fix{ Version: "", // important! no version because the status is not "fixed" State: db.NotAffectedFixStatus, Detail: nil, // important! no detail because the status is not "fixed" }, }, }, }, }, }, wantErr: false, }, { name: "multiple products sorted alphabetically", vuln: govex.Statement{ ID: "test-vuln-3", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@1.26.16", }, }, }, { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:npm/express@4.18.2", }, }, }, }, Status: govex.StatusNotAffected, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, want: []any{ db.UnaffectedPackageHandle{ Package: &db.Package{ Ecosystem: "npm", Name: "express", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.SemanticFormat.String(), Constraint: "= 4.18.2", }, Fix: &db.Fix{ State: db.NotAffectedFixStatus, }, }, }, }, }, db.UnaffectedPackageHandle{ Package: &db.Package{ // converts pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 1.26.16", }, Fix: &db.Fix{ State: db.NotAffectedFixStatus, }, }, }, }, }, }, wantErr: false, }, { name: "fixed status product", vuln: govex.Statement{ ID: "test-vuln-4", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "pkg:pypi/urllib3@2.0.7", }, }, }, }, Status: govex.StatusFixed, Vulnerability: govex.Vulnerability{ Name: "cve-2023-43804", Aliases: []govex.VulnerabilityID{"ghsa-v845-jxx5-vc9f"}, }, }, want: []any{ db.UnaffectedPackageHandle{ Package: &db.Package{ // converts pypi -> python Ecosystem: "python", Name: "urllib3", }, BlobValue: &db.PackageBlob{ CVEs: []string{"cve-2023-43804", "ghsa-v845-jxx5-vc9f"}, Ranges: []db.Range{ { Version: db.Version{ Type: version.PythonFormat.String(), Constraint: "= 2.0.7", }, Fix: &db.Fix{ Version: "2.0.7", State: db.FixedStatus, }, }, }, }, }, }, wantErr: false, }, { name: "invalid purl returns error", vuln: govex.Statement{ ID: "test-vuln-5", Products: []govex.Product{ { Component: govex.Component{ Identifiers: map[govex.IdentifierType]string{ govex.PURL: "invalid-purl", }, }, }, }, Status: govex.StatusAffected, }, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := getPackageHandles(&tt.vuln, tt.fixes) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("GetPackages() mismatch (-want +got):\n%s", diff) } }) } } ================================================ FILE: grype/db/v6/build/transformers/os/testdata/alpine-3.9.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "xen", "NamespaceName": "alpine:3.9", "Version": "4.11.1-r0", "VersionFormat": "apk", "Available": { "Date": "2018-12-01T09:15:30Z", "Kind": "package" } } ], "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", "Metadata": { "NVD": { "CVSSv2": { "Score": 4.9, "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" } } }, "Name": "CVE-2018-19967", "NamespaceName": "alpine:3.9", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/amazon-multiple-kernel-advisories.json ================================================ [ { "Vulnerability": { "Name": "ALAS-2021-1704", "NamespaceName": "amzn:2", "Description": "", "Severity": "Medium", "Metadata": { "CVE": [ { "Name": "CVE-2021-3653" }, { "Name": "CVE-2021-3656" }, { "Name": "CVE-2021-3732" } ] }, "Link": "https://alas.aws.amazon.com/AL2/ALAS-2021-1704.html", "FixedIn": [ { "Name": "kernel-headers", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "4.14.246-187.474.amzn2" }, { "Name": "kernel", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "4.14.246-187.474.amzn2" } ] } }, { "Vulnerability": { "Name": "ALASKERNEL-5.4-2022-007", "NamespaceName": "amzn:2", "Description": "", "Severity": "Medium", "Metadata": { "CVE": [ { "Name": "CVE-2021-3753" }, { "Name": "CVE-2021-40490" } ] }, "Link": "https://alas.aws.amazon.com/AL2/ALASKERNEL-5.4-2022-007.html", "FixedIn": [ { "Name": "kernel-headers", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.4.144-69.257.amzn2" }, { "Name": "kernel", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.4.144-69.257.amzn2" } ] } }, { "Vulnerability": { "Name": "ALASKERNEL-5.10-2022-005", "NamespaceName": "amzn:2", "Description": "", "Severity": "Medium", "Metadata": { "CVE": [ { "Name": "CVE-2021-3753" }, { "Name": "CVE-2021-40490" } ] }, "Link": "https://alas.aws.amazon.com/AL2/ALASKERNEL-5.10-2022-005.html", "FixedIn": [ { "Name": "kernel-headers", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.10.62-55.141.amzn2" }, { "Name": "kernel", "NamespaceName": "amzn:2", "VersionFormat": "rpm", "Version": "5.10.62-55.141.amzn2" } ] } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/amzn.json ================================================ [ { "Vulnerability": { "Description": "", "FixedIn": [ { "Name": "389-ds-base", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-debuginfo", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-devel", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-libs", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" }, { "Name": "389-ds-base-snmp", "NamespaceName": "amzn:2", "Version": "1.3.8.4-15.amzn2.0.1", "VersionFormat": "rpm" } ], "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", "Metadata": { "CVE": [ { "Name": "CVE-2018-14648" } ] }, "Name": "ALAS-2018-1106", "NamespaceName": "amzn:2", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/azure-linux-3.json ================================================ [ { "Vulnerability": { "Name": "CVE-2023-29403", "NamespaceName": "mariner:3.0", "Description": "CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", "Severity": "High", "Link": "https://nvd.nist.gov/vuln/detail/CVE-2023-29403", "CVSS": [], "FixedIn": [ { "Name": "golang", "NamespaceName": "mariner:3.0", "VersionFormat": "rpm", "Version": "0:1.20.7-1.azl3", "Module": "", "VendorAdvisory": { "NoAdvisory": false, "AdvisorySummary": [] } } ], "Metadata": {} } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/debian-8-multiple-entries-for-same-package.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "rsyslog", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "5.7.4-1", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", "Metadata": { "NVD": { "CVSSv2": { "Score": 2.1, "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" } } }, "Name": "CVE-2011-4623", "NamespaceName": "debian:8", "Severity": "Low" } }, { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "rsyslog", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "3.18.6-1", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", "Metadata": { "NVD": { "CVSSv2": { "Score": 5, "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" } } }, "Name": "CVE-2008-5618", "NamespaceName": "debian:8", "Severity": "Low" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/debian-8.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "asterisk", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "1:1.6.2.0~rc3-1", "VersionFormat": "dpkg" }, { "Name": "auth2db", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "0.2.5-2+dfsg-1", "VersionFormat": "dpkg" }, { "Name": "exaile", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "0.2.14+debian-2.2", "VersionFormat": "dpkg" }, { "Name": "wordpress", "NamespaceName": "debian:8", "VendorAdvisory": { "AdvisorySummary": [], "NoAdvisory": false }, "Version": "", "VersionFormat": "dpkg" } ], "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", "Metadata": { "NVD": { "CVSSv2": { "Score": 7.5, "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" } } }, "Name": "CVE-2008-7220", "NamespaceName": "debian:8", "Severity": "High" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/fedora-39.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "Security update for glib2 to fix CVE-2024-34397", "FixedIn": [ { "Name": "glib2", "NamespaceName": "fedora:39", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "FEDORA-2024-fd2569c4e9", "Link": "https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9" } ], "NoAdvisory": false }, "Version": "0:2.78.6-1.fc39", "VersionFormat": "rpm" } ], "Link": "https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9", "Metadata": { "Issued": "2024-05-09T02:43:30Z", "Updated": "2024-05-14T03:27:20Z" }, "Name": "CVE-2024-34397", "NamespaceName": "fedora:39", "Severity": "High" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/mariner-20.json ================================================ [ { "Vulnerability": { "Name": "CVE-2021-37621", "NamespaceName": "mariner:2.0", "Description": "CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.", "Severity": "Medium", "Link": "https://nvd.nist.gov/vuln/detail/CVE-2021-37621", "CVSS": [], "FixedIn": [ { "Name": "exiv2", "NamespaceName": "mariner:2.0", "VersionFormat": "rpm", "Version": "0:0.27.5-1.cm2", "Module": "", "VendorAdvisory": { "NoAdvisory": false, "AdvisorySummary": [] } } ], "Metadata": {} } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/mariner-range.json ================================================ [ { "Vulnerability": { "Name": "CVE-2023-29404", "NamespaceName": "mariner:2.0", "Description": "CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", "Severity": "Critical", "Link": "https://nvd.nist.gov/vuln/detail/CVE-2023-29404", "CVSS": [], "FixedIn": [ { "Name": "golang", "NamespaceName": "mariner:2.0", "VersionFormat": "rpm", "Version": "0:1.20.7-1.cm2", "Module": "", "VendorAdvisory": { "NoAdvisory": false, "AdvisorySummary": [] }, "VulnerableRange": "> 0:1.19.0.cm2, < 0:1.20.7-1.cm2" } ], "Metadata": {} } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/ol-8-modules.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", "FixedIn": [ { "Module": "postgresql:10", "Name": "postgresql", "NamespaceName": "ol:8", "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", "VersionFormat": "rpm" }, { "Module": "postgresql:12", "Name": "postgresql", "NamespaceName": "ol:8", "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", "VersionFormat": "rpm" }, { "Module": "postgresql:9.6", "Name": "postgresql", "NamespaceName": "ol:8", "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", "Metadata": {}, "Name": "CVE-2020-14350", "NamespaceName": "ol:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/ol-8.json ================================================ [ { "Vulnerability": { "CVSS": [], "Description": "", "FixedIn": [ { "Name": "libexif", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-devel", "NamespaceName": "ol:8", "Version": "0:0.6.21-17.el8_2", "VersionFormat": "rpm" }, { "Name": "libexif-dummy", "NamespaceName": "ol:8", "Version": "None", "VersionFormat": "rpm" } ], "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", "Metadata": { "CVE": [ { "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", "Name": "CVE-2020-13112" } ], "Issued": "2020-06-15", "RefId": "ELSA-2020-2550" }, "Name": "ELSA-2020-2550", "NamespaceName": "ol:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/rhel-8-modules.json ================================================ [ { "Vulnerability": { "CVSS": [ { "base_metrics": { "base_score": 7.1, "base_severity": "High", "exploitability_score": 1.2, "impact_score": 5.9 }, "status": "verified", "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", "version": "3.1" } ], "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", "FixedIn": [ { "Module": "postgresql:10", "Name": "postgresql", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:3669", "Link": "https://access.redhat.com/errata/RHSA-2020:3669" } ], "NoAdvisory": false }, "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", "VersionFormat": "rpm" }, { "Module": "postgresql:12", "Name": "postgresql", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:5620", "Link": "https://access.redhat.com/errata/RHSA-2020:5620" } ], "NoAdvisory": false }, "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", "VersionFormat": "rpm" }, { "Module": "postgresql:9.6", "Name": "postgresql", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:5619", "Link": "https://access.redhat.com/errata/RHSA-2020:5619" } ], "NoAdvisory": false }, "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", "Metadata": {}, "Name": "CVE-2020-14350", "NamespaceName": "rhel:8", "Severity": "Medium" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/testdata/rhel-8.json ================================================ [ { "Vulnerability": { "CVSS": [ { "base_metrics": { "base_score": 8.8, "base_severity": "High", "exploitability_score": 2.8, "impact_score": 5.9 }, "status": "verified", "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", "version": "3.1" } ], "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", "FixedIn": [ { "Name": "firefox", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:1341", "Link": "https://access.redhat.com/errata/RHSA-2020:1341" } ], "NoAdvisory": false }, "Version": "0:68.6.1-1.el8_1", "VersionFormat": "rpm", "Available": { "Date": "2020-04-08T14:30:15Z", "Kind": "advisory" } }, { "Name": "thunderbird", "NamespaceName": "rhel:8", "VendorAdvisory": { "AdvisorySummary": [ { "ID": "RHSA-2020:1495", "Link": "https://access.redhat.com/errata/RHSA-2020:1495" } ], "NoAdvisory": false }, "Version": "0:68.7.0-1.el8_1", "VersionFormat": "rpm" } ], "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", "Metadata": {}, "Name": "CVE-2020-6819", "NamespaceName": "rhel:8", "Severity": "Critical" } } ] ================================================ FILE: grype/db/v6/build/transformers/os/transform.go ================================================ package os // nolint:revive import ( "fmt" "sort" "strconv" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/codename" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/db/v6/name" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/pkg" ) // advisoryKey is an internal struct used for sorting and deduplicating advisories // that have both a link and ID from the vunnel results data type advisoryKey struct { id string link string } func Transform(vulnerability unmarshal.OSVulnerability, state provider.State) ([]data.Entry, error) { in := []any{ db.VulnerabilityHandle{ Name: vulnerability.Vulnerability.Name, ProviderID: state.Provider, Provider: provider.Model(state), Status: db.VulnerabilityActive, ModifiedDate: internal.ParseTime(vulnerability.Vulnerability.Metadata.Updated), PublishedDate: internal.ParseTime(vulnerability.Vulnerability.Metadata.Issued), BlobValue: &db.VulnerabilityBlob{ ID: vulnerability.Vulnerability.Name, Assigners: nil, Description: strings.TrimSpace(vulnerability.Vulnerability.Description), References: getReferences(vulnerability), Aliases: getAliases(vulnerability), Severities: getSeverities(vulnerability), }, }, } for _, a := range getAffectedPackages(vulnerability) { in = append(in, a) } return transformers.NewEntries(in...), nil } func getAffectedPackages(vuln unmarshal.OSVulnerability) []db.AffectedPackageHandle { var afs []db.AffectedPackageHandle groups := groupFixedIns(vuln) for group, fixedIns := range groups { // we only care about a single qualifier: rpm modules. The important thing to note about this is that // a package with no module vs a package with a module should be detectable in the DB. var qualifiers *db.PackageQualifiers if group.format == "rpm" { module := "" // means the target package must have no module (where as nil means the module has no sway on matching) if group.hasModule { module = group.module } qualifiers = &db.PackageQualifiers{ RpmModularity: &module, } } aph := db.AffectedPackageHandle{ OperatingSystem: getOperatingSystem(group.osName, group.id, group.osVersion, group.osChannel), Package: getPackage(group), BlobValue: &db.PackageBlob{ CVEs: getAliases(vuln), Qualifiers: qualifiers, Ranges: nil, }, } var ranges []db.Range for _, fixedInEntry := range fixedIns { ranges = append(ranges, db.Range{ Version: db.Version{ Type: fixedInEntry.VersionFormat, Constraint: enforceConstraint(fixedInEntry.Version, fixedInEntry.VulnerableRange, fixedInEntry.VersionFormat, vuln.Vulnerability.Name), }, Fix: getFix(fixedInEntry), }) } aph.BlobValue.Ranges = ranges afs = append(afs, aph) } // stable ordering sort.Sort(internal.ByAffectedPackage(afs)) return afs } func getFix(fixedInEntry unmarshal.OSFixedIn) *db.Fix { fixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Version) fixState := db.NotFixedStatus if len(fixedInVersion) > 0 { fixState = db.FixedStatus } else if fixedInEntry.VendorAdvisory.NoAdvisory { fixState = db.WontFixStatus } var advisoryOrder []advisoryKey advisorySet := strset.New() for _, a := range fixedInEntry.VendorAdvisory.AdvisorySummary { if a.Link != "" && !advisorySet.Has(a.Link) { advisoryOrder = append(advisoryOrder, advisoryKey{id: a.ID, link: a.Link}) advisorySet.Add(a.Link) } } var refs []db.Reference for _, adv := range advisoryOrder { refs = append(refs, db.Reference{ ID: adv.id, URL: adv.link, Tags: []string{db.AdvisoryReferenceTag}, }) } var detail *db.FixDetail availability := getFixAvailability(fixedInEntry) if len(refs) > 0 || availability != nil { detail = &db.FixDetail{ Available: availability, References: refs, } } return &db.Fix{ Version: fixedInVersion, State: fixState, Detail: detail, } } func getFixAvailability(fixedInEntry unmarshal.OSFixedIn) *db.FixAvailability { if fixedInEntry.Available.Date == "" { return nil } t := internal.ParseTime(fixedInEntry.Available.Date) if t == nil { log.WithFields("date", fixedInEntry.Available.Date).Warn("unable to parse fix availability date") return nil } return &db.FixAvailability{ Date: t, Kind: fixedInEntry.Available.Kind, } } func enforceConstraint(fixedVersion, vulnerableRange, format, vulnerabilityID string) string { if len(vulnerableRange) > 0 { return vulnerableRange } fixedVersion = versionutil.CleanConstraint(fixedVersion) if len(fixedVersion) == 0 { return "" } switch strings.ToLower(format) { case "semver": return versionutil.EnforceSemVerConstraint(fixedVersion) default: // the passed constraint is a fixed version return deriveConstraintFromFix(fixedVersion, vulnerabilityID) } } func deriveConstraintFromFix(fixVersion, vulnerabilityID string) string { constraint := fmt.Sprintf("< %s", fixVersion) if strings.HasPrefix(vulnerabilityID, "ALASKERNEL-") { // Amazon advisories of the form ALASKERNEL-5.4-2023-048 should be interpreted as only applying to // the 5.4.x kernel line since Amazon issue a separate advisory per affected line, thus the constraint // should be >= 5.4, < {fix version}. In the future the vunnel schema for OS vulns should be enhanced // to emit actual constraints rather than fixed-in entries (tracked in https://github.com/anchore/vunnel/issues/266) // at which point this workaround in grype-db can be removed. components := strings.Split(vulnerabilityID, "-") if len(components) == 4 { base := components[1] constraint = fmt.Sprintf(">= %s, < %s", base, fixVersion) } } return constraint } type groupIndex struct { name string id string osName string osVersion string osChannel string hasModule bool module string format string } func groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OSFixedIn { grouped := make(map[groupIndex][]unmarshal.OSFixedIn) oi := getOSInfo(vuln.Vulnerability.NamespaceName) for _, fixedIn := range vuln.Vulnerability.FixedIn { var mod string if fixedIn.Module != nil { mod = *fixedIn.Module } g := groupIndex{ name: fixedIn.Name, id: oi.id, osName: oi.name, osVersion: oi.version, osChannel: oi.channel, hasModule: fixedIn.Module != nil, module: mod, format: fixedIn.VersionFormat, } grouped[g] = append(grouped[g], fixedIn) } return grouped } func getPackageType(osName string) pkg.Type { switch osName { case "redhat", "amazonlinux", "oraclelinux", "sles", "mariner", "azurelinux", "photon", "fedora", "rocky", "rockylinux", "almalinux", "centos": return pkg.RpmPkg case "ubuntu", "debian", "echo": return pkg.DebPkg case "alpine", "chainguard", "wolfi", "minimos", "secureos": return pkg.ApkPkg case "windows": return pkg.KbPkg } return "" } func getPackage(group groupIndex) *db.Package { t := getPackageType(group.osName) return &db.Package{ Ecosystem: string(t), Name: name.Normalize(group.name, t), } } type osInfo struct { name string id string version string channel string } func getOSInfo(group string) osInfo { // derived from enterprise feed groups, expected to be of the form {distro release ID}:{version} feedGroupComponents := strings.Split(group, ":") id := feedGroupComponents[0] version := feedGroupComponents[1] channel := "" if strings.Contains(feedGroupComponents[1], "+") { versionParts := strings.Split(feedGroupComponents[1], "+") channel = versionParts[1] version = versionParts[0] } if strings.ToLower(id) == "mariner" { verFields := strings.Split(version, ".") majorVersionStr := verFields[0] majorVer, err := strconv.Atoi(majorVersionStr) if err == nil { if majorVer >= 3 { id = string(distro.Azure) } } } return osInfo{ name: normalizeOsName(id), id: id, version: version, channel: channel, } } func normalizeOsName(id string) string { d, ok := distro.IDMapping[id] if !ok { log.WithFields("distro", id).Warn("unknown distro name") return id } return d.String() } func getOperatingSystem(osName, osID, osVersion, channel string) *db.OperatingSystem { if osName == "" || osVersion == "" { return nil } versionFields := strings.Split(osVersion, ".") var majorVersion, minorVersion, labelVersion string majorVersion = versionFields[0] if len(majorVersion) > 0 { // is the first field a number? _, err := strconv.Atoi(majorVersion[0:1]) if err != nil { labelVersion = majorVersion majorVersion = "" } else if len(versionFields) > 1 { minorVersion = versionFields[1] } } return &db.OperatingSystem{ Name: osName, ReleaseID: osID, MajorVersion: majorVersion, MinorVersion: minorVersion, LabelVersion: labelVersion, Channel: channel, Codename: codename.LookupOS(osName, majorVersion, minorVersion), } } func getReferences(vuln unmarshal.OSVulnerability) []db.Reference { clean := strings.TrimSpace(vuln.Vulnerability.Link) if clean == "" { return nil } var linkOrder []string linkSet := strset.New() if vuln.Vulnerability.Link != "" { linkSet.Add(vuln.Vulnerability.Link) linkOrder = append(linkOrder, vuln.Vulnerability.Link) } for _, a := range vuln.Vulnerability.Metadata.CVE { if a.Link != "" && !linkSet.Has(a.Link) { linkOrder = append(linkOrder, a.Link) } } var refs []db.Reference for _, l := range linkOrder { refs = append(refs, db.Reference{ URL: l, }, ) } return refs } func getAliases(vuln unmarshal.OSVulnerability) []string { var aliases []string for _, cve := range vuln.Vulnerability.Metadata.CVE { aliases = append(aliases, cve.Name, ) } return aliases } func getSeverities(vuln unmarshal.OSVulnerability) []db.Severity { var severities []db.Severity // TODO: should we clean this here or not? if vuln.Vulnerability.Severity != "" && strings.ToLower(vuln.Vulnerability.Severity) != "unknown" { severities = append(severities, db.Severity{ Scheme: db.SeveritySchemeCHMLN, Value: strings.ToLower(vuln.Vulnerability.Severity), Rank: 1, // TODO: enum this // TODO Source? }) } for _, vendorSeverity := range vuln.Vulnerability.CVSS { severities = append(severities, db.Severity{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: vendorSeverity.VectorString, Version: vendorSeverity.Version, }, Rank: 2, // TODO: source? }) } return severities } ================================================ FILE: grype/db/v6/build/transformers/os/transform_test.go ================================================ package os import ( "os" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" ) var timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) var listing = provider.File{ Path: "some", Digest: "123456", Algorithm: "sha256", } func inputProviderState(name string) provider.State { return provider.State{ Provider: name, Version: 12, Processor: "vunnel@1.2.3", Timestamp: timeVal, Listing: &listing, } } func expectedProvider(name string) *db.Provider { return &db.Provider{ ID: name, Version: "12", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", } } func TestTransform(t *testing.T) { alpineOS := &db.OperatingSystem{ Name: "alpine", ReleaseID: "alpine", MajorVersion: "3", MinorVersion: "9", } amazonOS := &db.OperatingSystem{ Name: "amazonlinux", ReleaseID: "amzn", MajorVersion: "2", } azure3OS := &db.OperatingSystem{ Name: "azurelinux", ReleaseID: "azurelinux", MajorVersion: "3", MinorVersion: "0", // TODO: is this right? } debian8OS := &db.OperatingSystem{ Name: "debian", ReleaseID: "debian", MajorVersion: "8", Codename: "jessie", } mariner2OS := &db.OperatingSystem{ Name: "mariner", ReleaseID: "mariner", MajorVersion: "2", MinorVersion: "0", // TODO: is this right? } ol8OS := &db.OperatingSystem{ Name: "oraclelinux", ReleaseID: "ol", MajorVersion: "8", } rhel8OS := &db.OperatingSystem{ Name: "redhat", ReleaseID: "rhel", MajorVersion: "8", } fedora39OS := &db.OperatingSystem{ Name: "fedora", ReleaseID: "fedora", MajorVersion: "39", } tests := []struct { name string provider string want []transformers.RelatedEntries }{ { name: "testdata/alpine-3.9.json", provider: "alpine", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2018-19967", Status: "active", ProviderID: "alpine", Provider: expectedProvider("alpine"), BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2018-19967", References: []db.Reference{ { URL: "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: alpineOS, Package: &db.Package{Ecosystem: "apk", Name: "xen"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "apk", Constraint: "< 4.11.1-r0"}, Fix: &db.Fix{ Version: "4.11.1-r0", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2018, 12, 1, 9, 15, 30, 0, time.UTC)), Kind: "package", }, }, }, }, }, }, }, ), }, }, }, { name: "testdata/amzn.json", provider: "amazon", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "ALAS-2018-1106", ProviderID: "amazon", Provider: expectedProvider("amazon"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "ALAS-2018-1106", References: []db.Reference{ { URL: "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", }, }, Aliases: []string{"CVE-2018-14648"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{ Name: "389-ds-base", Ecosystem: "rpm", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-14648"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 1.3.8.4-15.amzn2.0.1"}, Fix: &db.Fix{Version: "1.3.8.4-15.amzn2.0.1", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{ Name: "389-ds-base-debuginfo", Ecosystem: "rpm", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-14648"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 1.3.8.4-15.amzn2.0.1"}, Fix: &db.Fix{Version: "1.3.8.4-15.amzn2.0.1", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{ Name: "389-ds-base-devel", Ecosystem: "rpm", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-14648"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 1.3.8.4-15.amzn2.0.1"}, Fix: &db.Fix{Version: "1.3.8.4-15.amzn2.0.1", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{ Name: "389-ds-base-libs", Ecosystem: "rpm", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-14648"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 1.3.8.4-15.amzn2.0.1"}, Fix: &db.Fix{Version: "1.3.8.4-15.amzn2.0.1", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{ Name: "389-ds-base-snmp", Ecosystem: "rpm", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2018-14648"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 1.3.8.4-15.amzn2.0.1"}, Fix: &db.Fix{Version: "1.3.8.4-15.amzn2.0.1", State: db.FixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/amazon-multiple-kernel-advisories.json", provider: "amazon", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "ALAS-2021-1704", ProviderID: "amazon", Provider: expectedProvider("amazon"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "ALAS-2021-1704", References: []db.Reference{ { URL: "https://alas.aws.amazon.com/AL2/ALAS-2021-1704.html", }, }, Aliases: []string{"CVE-2021-3653", "CVE-2021-3656", "CVE-2021-3732"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{Ecosystem: "rpm", Name: "kernel"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2021-3653", "CVE-2021-3656", "CVE-2021-3732"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 4.14.246-187.474.amzn2"}, Fix: &db.Fix{Version: "4.14.246-187.474.amzn2", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{Ecosystem: "rpm", Name: "kernel-headers"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2021-3653", "CVE-2021-3656", "CVE-2021-3732"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 4.14.246-187.474.amzn2"}, Fix: &db.Fix{Version: "4.14.246-187.474.amzn2", State: db.FixedStatus}, }, }, }, }, ), }, { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "ALASKERNEL-5.4-2022-007", ProviderID: "amazon", Provider: expectedProvider("amazon"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "ALASKERNEL-5.4-2022-007", References: []db.Reference{ { URL: "https://alas.aws.amazon.com/AL2/ALASKERNEL-5.4-2022-007.html", }, }, Aliases: []string{"CVE-2021-3753", "CVE-2021-40490"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{Ecosystem: "rpm", Name: "kernel"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2021-3753", "CVE-2021-40490"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: ">= 5.4, < 5.4.144-69.257.amzn2"}, Fix: &db.Fix{Version: "5.4.144-69.257.amzn2", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{Ecosystem: "rpm", Name: "kernel-headers"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2021-3753", "CVE-2021-40490"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: ">= 5.4, < 5.4.144-69.257.amzn2"}, Fix: &db.Fix{Version: "5.4.144-69.257.amzn2", State: db.FixedStatus}, }, }, }, }, ), }, { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "ALASKERNEL-5.10-2022-005", ProviderID: "amazon", Provider: expectedProvider("amazon"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "ALASKERNEL-5.10-2022-005", References: []db.Reference{ { URL: "https://alas.aws.amazon.com/AL2/ALASKERNEL-5.10-2022-005.html", }, }, Aliases: []string{"CVE-2021-3753", "CVE-2021-40490"}, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{Ecosystem: "rpm", Name: "kernel"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2021-3753", "CVE-2021-40490"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: ">= 5.10, < 5.10.62-55.141.amzn2"}, Fix: &db.Fix{Version: "5.10.62-55.141.amzn2", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: amazonOS, Package: &db.Package{Ecosystem: "rpm", Name: "kernel-headers"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2021-3753", "CVE-2021-40490"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: ">= 5.10, < 5.10.62-55.141.amzn2"}, Fix: &db.Fix{Version: "5.10.62-55.141.amzn2", State: db.FixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/azure-linux-3.json", provider: "mariner", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-29403", ProviderID: "mariner", Provider: expectedProvider("mariner"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-29403", Description: "CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-29403", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "high", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: azure3OS, Package: &db.Package{Ecosystem: "rpm", Name: "golang"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 0:1.20.7-1.azl3"}, Fix: &db.Fix{Version: "0:1.20.7-1.azl3", State: db.FixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/debian-8.json", provider: "debian", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2008-7220", ProviderID: "debian", Provider: expectedProvider("debian"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2008-7220", References: []db.Reference{ { URL: "https://security-tracker.debian.org/tracker/CVE-2008-7220", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "high", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: debian8OS, Package: &db.Package{Ecosystem: "deb", Name: "asterisk"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "dpkg", Constraint: "< 1:1.6.2.0~rc3-1"}, Fix: &db.Fix{Version: "1:1.6.2.0~rc3-1", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: debian8OS, Package: &db.Package{Ecosystem: "deb", Name: "auth2db"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "dpkg", Constraint: "< 0.2.5-2+dfsg-1"}, Fix: &db.Fix{Version: "0.2.5-2+dfsg-1", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: debian8OS, Package: &db.Package{Ecosystem: "deb", Name: "exaile"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "dpkg", Constraint: "< 0.2.14+debian-2.2"}, Fix: &db.Fix{Version: "0.2.14+debian-2.2", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: debian8OS, Package: &db.Package{Ecosystem: "deb", Name: "wordpress"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "dpkg", Constraint: ""}, Fix: &db.Fix{Version: "", State: db.NotFixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/debian-8-multiple-entries-for-same-package.json", provider: "debian", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2011-4623", ProviderID: "debian", Provider: expectedProvider("debian"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2011-4623", References: []db.Reference{ { URL: "https://security-tracker.debian.org/tracker/CVE-2011-4623", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "low", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: debian8OS, Package: &db.Package{Ecosystem: "deb", Name: "rsyslog"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "dpkg", Constraint: "< 5.7.4-1"}, Fix: &db.Fix{Version: "5.7.4-1", State: db.FixedStatus}, }, }, }, }, ), }, { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2008-5618", ProviderID: "debian", Provider: expectedProvider("debian"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2008-5618", References: []db.Reference{ { URL: "https://security-tracker.debian.org/tracker/CVE-2008-5618", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "low", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: debian8OS, Package: &db.Package{Ecosystem: "deb", Name: "rsyslog"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Version: db.Version{Type: "dpkg", Constraint: "< 3.18.6-1"}, Fix: &db.Fix{Version: "3.18.6-1", State: db.FixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/mariner-20.json", provider: "mariner", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2021-37621", ProviderID: "mariner", Provider: expectedProvider("mariner"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2021-37621", Description: "CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2021-37621", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: mariner2OS, Package: &db.Package{Ecosystem: "rpm", Name: "exiv2"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 0:0.27.5-1.cm2"}, Fix: &db.Fix{Version: "0:0.27.5-1.cm2", State: db.FixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/mariner-range.json", provider: "mariner", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-29404", ProviderID: "mariner", Provider: expectedProvider("mariner"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-29404", Description: "CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.", References: []db.Reference{ { URL: "https://nvd.nist.gov/vuln/detail/CVE-2023-29404", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "critical", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: mariner2OS, Package: &db.Package{Ecosystem: "rpm", Name: "golang"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "> 0:1.19.0.cm2, < 0:1.20.7-1.cm2"}, Fix: &db.Fix{Version: "0:1.20.7-1.cm2", State: db.FixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/ol-8.json", provider: "oracle", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "ELSA-2020-2550", ProviderID: "oracle", Provider: expectedProvider("oracle"), Status: "active", PublishedDate: timeRef(time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC)), BlobValue: &db.VulnerabilityBlob{ ID: "ELSA-2020-2550", Aliases: []string{"CVE-2020-13112"}, References: []db.Reference{ { URL: "http://linux.oracle.com/errata/ELSA-2020-2550.html", }, { URL: "http://linux.oracle.com/cve/CVE-2020-13112.html", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: ol8OS, Package: &db.Package{Ecosystem: "rpm", Name: "libexif"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-13112"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 0:0.6.21-17.el8_2"}, Fix: &db.Fix{Version: "0:0.6.21-17.el8_2", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: ol8OS, Package: &db.Package{Ecosystem: "rpm", Name: "libexif-devel"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-13112"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: "< 0:0.6.21-17.el8_2"}, Fix: &db.Fix{Version: "0:0.6.21-17.el8_2", State: db.FixedStatus}, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: ol8OS, Package: &db.Package{Ecosystem: "rpm", Name: "libexif-dummy"}, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-13112"}, Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{Type: "rpm", Constraint: ""}, Fix: &db.Fix{State: db.NotFixedStatus}, }, }, }, }, ), }, }, }, { name: "testdata/ol-8-modules.json", provider: "oracle", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2020-14350", ProviderID: "oracle", Provider: expectedProvider("oracle"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2020-14350", Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", References: []db.Reference{ { URL: "https://access.redhat.com/security/cve/CVE-2020-14350", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: ol8OS, Package: &db.Package{Ecosystem: "rpm", Name: "postgresql"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{ RpmModularity: strRef("postgresql:10"), }, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:10.14-1.module+el8.2.0+7801+be0fed80", }, Fix: &db.Fix{ Version: "0:10.14-1.module+el8.2.0+7801+be0fed80", State: db.FixedStatus, }, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: ol8OS, Package: &db.Package{Ecosystem: "rpm", Name: "postgresql"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{ RpmModularity: strRef("postgresql:12"), }, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", }, Fix: &db.Fix{ Version: "0:12.5-1.module+el8.3.0+9042+664538f4", State: db.FixedStatus, }, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: ol8OS, Package: &db.Package{Ecosystem: "rpm", Name: "postgresql"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{ RpmModularity: strRef("postgresql:9.6"), }, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", }, Fix: &db.Fix{ Version: "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", State: db.FixedStatus, }, }, }, }, }, ), }, }, }, { name: "testdata/rhel-8.json", provider: "redhat", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2020-6819", ProviderID: "redhat", Provider: expectedProvider("redhat"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2020-6819", Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", References: []db.Reference{ { URL: "https://access.redhat.com/security/cve/CVE-2020-6819", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "critical", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", Version: "3.1", }, Rank: 2, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: rhel8OS, Package: &db.Package{Ecosystem: "rpm", Name: "firefox"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:68.6.1-1.el8_1", }, Fix: &db.Fix{ Version: "0:68.6.1-1.el8_1", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2020, 4, 8, 14, 30, 15, 0, time.UTC)), Kind: "advisory", }, References: []db.Reference{ { ID: "RHSA-2020:1341", URL: "https://access.redhat.com/errata/RHSA-2020:1341", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: rhel8OS, Package: &db.Package{Ecosystem: "rpm", Name: "thunderbird"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:68.7.0-1.el8_1", }, Fix: &db.Fix{ Version: "0:68.7.0-1.el8_1", State: db.FixedStatus, Detail: &db.FixDetail{ References: []db.Reference{ { ID: "RHSA-2020:1495", URL: "https://access.redhat.com/errata/RHSA-2020:1495", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, }, }, ), }, }, }, { name: "testdata/rhel-8-modules.json", provider: "redhat", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2020-14350", ProviderID: "redhat", Provider: expectedProvider("redhat"), Status: "active", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2020-14350", Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", References: []db.Reference{ { URL: "https://access.redhat.com/security/cve/CVE-2020-14350", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "medium", Rank: 1, }, { Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", Version: "3.1", }, Rank: 2, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: rhel8OS, Package: &db.Package{Ecosystem: "rpm", Name: "postgresql"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{ RpmModularity: strRef("postgresql:10"), }, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:10.14-1.module+el8.2.0+7801+be0fed80", }, Fix: &db.Fix{ Version: "0:10.14-1.module+el8.2.0+7801+be0fed80", State: db.FixedStatus, Detail: &db.FixDetail{ References: []db.Reference{ { ID: "RHSA-2020:3669", URL: "https://access.redhat.com/errata/RHSA-2020:3669", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: rhel8OS, Package: &db.Package{Ecosystem: "rpm", Name: "postgresql"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{ RpmModularity: strRef("postgresql:12"), }, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", }, Fix: &db.Fix{ Version: "0:12.5-1.module+el8.3.0+9042+664538f4", State: db.FixedStatus, Detail: &db.FixDetail{ References: []db.Reference{ { ID: "RHSA-2020:5620", URL: "https://access.redhat.com/errata/RHSA-2020:5620", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, }, }, db.AffectedPackageHandle{ OperatingSystem: rhel8OS, Package: &db.Package{Ecosystem: "rpm", Name: "postgresql"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{ RpmModularity: strRef("postgresql:9.6"), }, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", }, Fix: &db.Fix{ Version: "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", State: db.FixedStatus, Detail: &db.FixDetail{ References: []db.Reference{ { ID: "RHSA-2020:5619", URL: "https://access.redhat.com/errata/RHSA-2020:5619", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, }, }, ), }, }, }, { name: "testdata/fedora-39.json", provider: "fedora", want: []transformers.RelatedEntries{ { VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2024-34397", ProviderID: "fedora", Provider: expectedProvider("fedora"), Status: "active", PublishedDate: timeRef(time.Date(2024, 5, 9, 2, 43, 30, 0, time.UTC)), ModifiedDate: timeRef(time.Date(2024, 5, 14, 3, 27, 20, 0, time.UTC)), BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2024-34397", Description: "Security update for glib2 to fix CVE-2024-34397", References: []db.Reference{ { URL: "https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9", }, }, Severities: []db.Severity{ { Scheme: db.SeveritySchemeCHMLN, Value: "high", Rank: 1, }, }, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ OperatingSystem: fedora39OS, Package: &db.Package{Ecosystem: "rpm", Name: "glib2"}, BlobValue: &db.PackageBlob{ Qualifiers: &db.PackageQualifiers{RpmModularity: strRef("")}, Ranges: []db.Range{ { Version: db.Version{ Type: "rpm", Constraint: "< 0:2.78.6-1.fc39", }, Fix: &db.Fix{ Version: "0:2.78.6-1.fc39", State: db.FixedStatus, Detail: &db.FixDetail{ References: []db.Reference{ { ID: "FEDORA-2024-fd2569c4e9", URL: "https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, }, }, ), }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { vulns := loadFixture(t, test.name) var actual []transformers.RelatedEntries for _, vuln := range vulns { entries, err := Transform(vuln, inputProviderState(test.provider)) require.NoError(t, err) for _, entry := range entries { e, ok := entry.Data.(transformers.RelatedEntries) require.True(t, ok) actual = append(actual, e) } } if diff := cmp.Diff(test.want, actual); diff != "" { t.Errorf("data entries mismatch (-want +got):\n%s", diff) } }) } } func TestGetOperatingSystem(t *testing.T) { tests := []struct { name string osName string osID string osVersion string channel string expected *db.OperatingSystem }{ { name: "works with given args", osName: "alpine", osID: "alpine", osVersion: "3.10", expected: &db.OperatingSystem{ Name: "alpine", ReleaseID: "alpine", MajorVersion: "3", MinorVersion: "10", LabelVersion: "", Codename: "", }, }, { name: "does codename lookup (debian)", osName: "debian", osID: "debian", osVersion: "11", expected: &db.OperatingSystem{ Name: "debian", ReleaseID: "debian", MajorVersion: "11", MinorVersion: "", LabelVersion: "", Codename: "bullseye", }, }, { name: "does codename lookup (ubuntu)", osName: "ubuntu", osID: "ubuntu", osVersion: "22.04", expected: &db.OperatingSystem{ Name: "ubuntu", ReleaseID: "ubuntu", MajorVersion: "22", MinorVersion: "04", LabelVersion: "", Codename: "jammy", }, }, { name: "includes channel (rhel)", osName: "redhat", osID: "rhel", osVersion: "8.4", channel: "eus", expected: &db.OperatingSystem{ Name: "redhat", ReleaseID: "rhel", MajorVersion: "8", MinorVersion: "4", Channel: "eus", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getOperatingSystem(tt.osName, tt.osID, tt.osVersion, tt.channel) require.Equal(t, tt.expected, result) }) } } func TestGetOSInfo(t *testing.T) { tests := []struct { name string group string expected osInfo }{ { name: "alpine 3.10", group: "alpine:3.10", expected: osInfo{ name: "alpine", id: "alpine", version: "3.10", }, }, { name: "debian bullseye", group: "debian:11", expected: osInfo{ name: "debian", id: "debian", version: "11", }, }, { name: "mariner version 1", group: "mariner:1.0", expected: osInfo{ name: "mariner", id: "mariner", version: "1.0", }, }, { name: "mariner version 3 (azurelinux conversion)", group: "mariner:3.0", expected: osInfo{ name: "azurelinux", id: "azurelinux", version: "3.0", }, }, { name: "ubuntu focal", group: "ubuntu:20.04", expected: osInfo{ name: "ubuntu", id: "ubuntu", version: "20.04", }, }, { name: "oracle linux", group: "ol:8", expected: osInfo{ name: "oraclelinux", // normalize name id: "ol", // keep original ID version: "8", }, }, { name: "redhat 8", group: "rhel:8", expected: osInfo{ name: "redhat", id: "rhel", version: "8", }, }, { name: "rhel + eus", group: "rhel:8+eus", expected: osInfo{ name: "redhat", id: "rhel", version: "8", channel: "eus", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { oi := getOSInfo(tt.group) assert.Equal(t, tt.expected, oi, "expected osInfo to match for group %s", tt.group) }) } } func TestGetFixAvailability(t *testing.T) { tests := []struct { name string fixture string expected map[string]*db.FixAvailability // keyed by package name for fixture-based testing }{ { name: "alpine-3.9 with package availability", fixture: "testdata/alpine-3.9.json", expected: map[string]*db.FixAvailability{ "xen": { Date: timeRef(time.Date(2018, 12, 1, 9, 15, 30, 0, time.UTC)), Kind: "package", }, }, }, { name: "rhel-8 with advisory availability", fixture: "testdata/rhel-8.json", expected: map[string]*db.FixAvailability{ "firefox": { Date: timeRef(time.Date(2020, 4, 8, 14, 30, 15, 0, time.UTC)), Kind: "advisory", }, "thunderbird": nil, // no availability data in fixture }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { vulnerabilities := loadFixture(t, tt.fixture) require.Len(t, vulnerabilities, 1, "expected exactly one vulnerability") for _, fixedIn := range vulnerabilities[0].Vulnerability.FixedIn { result := getFixAvailability(fixedIn) expected := tt.expected[fixedIn.Name] if expected == nil { require.Nil(t, result, "expected nil availability for %s", fixedIn.Name) } else { require.NotNil(t, result, "expected non-nil availability for %s", fixedIn.Name) require.Equal(t, expected.Kind, result.Kind) require.Equal(t, expected.Date, result.Date) } } }) } // keep edge case test for scenarios not covered by fixtures t.Run("invalid date returns nil", func(t *testing.T) { fixedIn := unmarshal.OSFixedIn{ Available: struct { Date string `json:"Date,omitempty"` Kind string `json:"Kind,omitempty"` }{ Date: "invalid-date", Kind: "commit", }, } result := getFixAvailability(fixedIn) require.Nil(t, result) }) } func TestGetFixWithDetail(t *testing.T) { tests := []struct { name string fixedIn unmarshal.OSFixedIn expected *db.Fix }{ { name: "fix with version and availability", fixedIn: unmarshal.OSFixedIn{ Version: "1.2.3", Available: struct { Date string `json:"Date,omitempty"` Kind string `json:"Kind,omitempty"` }{ Date: "2023-01-15T10:30:45Z", Kind: "advisory", }, VendorAdvisory: struct { AdvisorySummary []struct { ID string `json:"ID"` Link string `json:"Link"` } `json:"AdvisorySummary"` NoAdvisory bool `json:"NoAdvisory"` }{ AdvisorySummary: []struct { ID string `json:"ID"` Link string `json:"Link"` }{ { ID: "RHSA-2023-001", Link: "https://access.redhat.com/errata/RHSA-2023-001", }, }, }, }, expected: &db.Fix{ Version: "1.2.3", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2023, 1, 15, 10, 30, 45, 0, time.UTC)), Kind: "advisory", }, References: []db.Reference{ { ID: "RHSA-2023-001", URL: "https://access.redhat.com/errata/RHSA-2023-001", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, { name: "fix with version but no availability or references", fixedIn: unmarshal.OSFixedIn{ Version: "2.0.0", Available: struct { Date string `json:"Date,omitempty"` Kind string `json:"Kind,omitempty"` }{}, }, expected: &db.Fix{ Version: "2.0.0", State: db.FixedStatus, Detail: nil, }, }, { name: "no fix version with availability", fixedIn: unmarshal.OSFixedIn{ Version: "", Available: struct { Date string `json:"Date,omitempty"` Kind string `json:"Kind,omitempty"` }{ Date: "2023-01-15T10:30:45Z", Kind: "release", }, }, expected: &db.Fix{ Version: "", State: db.NotFixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2023, 1, 15, 10, 30, 45, 0, time.UTC)), Kind: "release", }, }, }, }, { name: "vendor advisory with no advisory flag set", fixedIn: unmarshal.OSFixedIn{ Version: "", Available: struct { Date string `json:"Date,omitempty"` Kind string `json:"Kind,omitempty"` }{}, VendorAdvisory: struct { AdvisorySummary []struct { ID string `json:"ID"` Link string `json:"Link"` } `json:"AdvisorySummary"` NoAdvisory bool `json:"NoAdvisory"` }{ NoAdvisory: true, }, }, expected: &db.Fix{ Version: "", State: db.WontFixStatus, Detail: nil, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getFix(tt.fixedIn) if d := cmp.Diff(tt.expected, result); d != "" { t.Fatalf("unexpected result: %s", d) } }) } } func TestGetFixWithDetailFixtures(t *testing.T) { // additional fixture-based tests to complement the existing ad-hoc tests tests := []struct { name string fixture string expected map[string]*db.Fix // keyed by package name }{ { name: "alpine-3.9 with availability", fixture: "testdata/alpine-3.9.json", expected: map[string]*db.Fix{ "xen": { Version: "4.11.1-r0", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2018, 12, 1, 9, 15, 30, 0, time.UTC)), Kind: "package", }, }, }, }, }, { name: "rhel-8 with availability and advisory references", fixture: "testdata/rhel-8.json", expected: map[string]*db.Fix{ "firefox": { Version: "0:68.6.1-1.el8_1", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2020, 4, 8, 14, 30, 15, 0, time.UTC)), Kind: "advisory", }, References: []db.Reference{ { ID: "RHSA-2020:1341", URL: "https://access.redhat.com/errata/RHSA-2020:1341", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, "thunderbird": { Version: "0:68.7.0-1.el8_1", State: db.FixedStatus, Detail: &db.FixDetail{ References: []db.Reference{ { ID: "RHSA-2020:1495", URL: "https://access.redhat.com/errata/RHSA-2020:1495", Tags: []string{db.AdvisoryReferenceTag}, }, }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { vulnerabilities := loadFixture(t, tt.fixture) require.Len(t, vulnerabilities, 1, "expected exactly one vulnerability") for _, fixedIn := range vulnerabilities[0].Vulnerability.FixedIn { result := getFix(fixedIn) expected := tt.expected[fixedIn.Name] require.NotNil(t, expected, "no expected result for package %s", fixedIn.Name) if d := cmp.Diff(expected, result); d != "" { t.Fatalf("unexpected result for %s: %s", fixedIn.Name, d) } } }) } } func affectedPkgSlice(a ...db.AffectedPackageHandle) []any { var r []any for _, v := range a { r = append(r, v) } return r } func loadFixture(t *testing.T, fixturePath string) []unmarshal.OSVulnerability { t.Helper() f, err := os.Open(fixturePath) require.NoError(t, err) defer func() { require.NoError(t, f.Close()) }() entries, err := unmarshal.OSVulnerabilityEntries(f) require.NoError(t, err) return entries } func timeRef(ti time.Time) *time.Time { return &ti } func strRef(s string) *string { return &s } ================================================ FILE: grype/db/v6/build/transformers/osv/testdata/ALSA-2025-7467.json ================================================ { "id": "ALSA-2025:7467", "summary": "Moderate: skopeo security update", "aliases": [ "CVE-2025-27144" ], "affected": [ { "package": { "ecosystem": "AlmaLinux:10", "name": "skopeo" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { "introduced": "0" }, { "fixed": "2:1.18.1-1.el10_0" } ] } ] }, { "package": { "ecosystem": "AlmaLinux:10", "name": "skopeo-tests" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { "introduced": "0" }, { "fixed": "2:1.18.1-1.el10_0" } ] } ] } ], "published": "2025-05-13T00:00:00Z", "modified": "2025-07-02T12:50:06Z", "details": "The skopeo command lets you inspect images from container image registries.", "references": [ { "url": "https://errata.almalinux.org/10/ALSA-2025-7467.html", "type": "ADVISORY" } ], "database_specific": { "anchore": { "record_type": "advisory" } } } ================================================ FILE: grype/db/v6/build/transformers/osv/testdata/BIT-apache-2020-11984.json ================================================ { "schema_version": "1.5.0", "id": "BIT-apache-2020-11984", "details": "Apache HTTP server 2.4.32 to 2.4.44 mod_proxy_uwsgi info disclosure and possible RCE", "aliases": [ "CVE-2020-11984" ], "affected": [ { "package": { "ecosystem": "Bitnami", "name": "apache", "purl": "pkg:bitnami/apache" }, "severity": [ { "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" } ], "ranges": [ { "type": "SEMVER", "events": [ { "introduced": "2.4.32" }, { "last_affected": "2.4.43" } ] } ] } ], "database_specific": { "severity": "Critical", "cpes": [ "cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:*" ] }, "references": [ { "type": "WEB", "url": "http://www.openwall.com/lists/oss-security/2020/08/08/1" }, { "type": "WEB", "url": "http://www.openwall.com/lists/oss-security/2020/08/08/10" } ], "published": "2024-03-06T10:57:57.770Z", "modified": "2025-01-17T15:26:01.971Z" } ================================================ FILE: grype/db/v6/build/transformers/osv/testdata/BIT-node-2020-8201.json ================================================ { "schema_version": "1.5.0", "id": "BIT-node-2020-8201", "details": "Node.js < 12.18.4 and < 14.11 can be exploited to perform HTTP desync attacks and deliver malicious payloads to unsuspecting users. The payloads can be crafted by an attacker to hijack user sessions, poison cookies, perform clickjacking, and a multitude of other attacks depending on the architecture of the underlying system. The attack was possible due to a bug in processing of carrier-return symbols in the HTTP header names.", "aliases": [ "CVE-2020-8201" ], "affected": [ { "package": { "ecosystem": "Bitnami", "name": "node", "purl": "pkg:bitnami/node" }, "severity": [ { "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N" } ], "ranges": [ { "type": "SEMVER", "events": [ { "introduced": "12.0.0" }, { "fixed": "12.18.4" }, { "introduced": "14.0.0" }, { "fixed": "14.11.0" } ], "database_specific": { "anchore": { "fixes": [ { "version": "12.18.4", "date": "2020-09-15", "kind": "first-observed" }, { "version": "14.11.0", "date": "2020-09-15", "kind": "first-observed" } ] } } } ] } ], "database_specific": { "severity": "High", "cpes": [ "cpe:2.3:a:nodejs:node.js:*:*:*:*:*:*:*:*", "cpe:2.3:a:nodejs:node.js:*:*:*:*:lts:*:*:*" ] }, "references": [ { "type": "WEB", "url": "https://nodejs.org/en/blog/vulnerability/september-2020-security-releases/" }, { "type": "WEB", "url": "https://security.gentoo.org/glsa/202101-07" } ], "published": "2024-03-06T11:08:09.371Z", "modified": "2024-03-06T11:25:28.861Z" } ================================================ FILE: grype/db/v6/build/transformers/osv/transform.go ================================================ package osv import ( "fmt" "regexp" "sort" "strconv" "strings" "github.com/google/osv-scanner/pkg/models" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/internal/codename" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/internal/versionutil" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/grype/db/v6/build/transformers/internal" "github.com/anchore/grype/grype/db/v6/name" "github.com/anchore/syft/syft/pkg" ) const ( almaLinux = "almalinux" ) func Transform(vulnerability unmarshal.OSVVulnerability, state provider.State) ([]data.Entry, error) { severities, err := getSeverities(vulnerability) if err != nil { return nil, fmt.Errorf("unable to obtain severities: %w", err) } isAdvisory := isAdvisoryRecord(vulnerability) aliases := vulnerability.Aliases if isAdvisory { aliases = append(aliases, vulnerability.Related...) } in := []any{ db.VulnerabilityHandle{ Name: vulnerability.ID, ProviderID: state.Provider, Provider: provider.Model(state), Status: db.VulnerabilityActive, ModifiedDate: &vulnerability.Modified, PublishedDate: &vulnerability.Published, BlobValue: &db.VulnerabilityBlob{ ID: vulnerability.ID, Assigners: nil, Description: vulnerability.Details, References: getReferences(vulnerability), Aliases: aliases, Severities: severities, }, }, } // Check if this is an advisory record if isAdvisory { // For advisory records, emit unaffected packages for _, u := range getUnaffectedPackages(vulnerability) { in = append(in, u) } } else { // For vulnerability records, emit affected packages for _, a := range getAffectedPackages(vulnerability) { in = append(in, a) } } return transformers.NewEntries(in...), nil } func getAffectedPackages(vuln unmarshal.OSVVulnerability) []db.AffectedPackageHandle { if len(vuln.Affected) == 0 { return nil } // CPES might be in the database_specific information cpes, withCPE := vuln.DatabaseSpecific["cpes"] if withCPE { if _, ok := cpes.([]string); !ok { withCPE = false } } var aphs []db.AffectedPackageHandle for _, affected := range vuln.Affected { aph := db.AffectedPackageHandle{ Package: getPackage(affected.Package), OperatingSystem: getOperatingSystemFromEcosystem(string(affected.Package.Ecosystem)), BlobValue: &db.PackageBlob{CVEs: vuln.Aliases}, } // Extract qualifiers (CPE and RPM modularity) qualifiers := getPackageQualifiers(affected, cpes, withCPE) if qualifiers != nil { aph.BlobValue.Qualifiers = qualifiers } var ranges []db.Range for _, r := range affected.Ranges { ranges = append(ranges, getGrypeRangesFromRange(r, string(affected.Package.Ecosystem))...) } aph.BlobValue.Ranges = ranges aphs = append(aphs, aph) } // stable ordering sort.Sort(internal.ByAffectedPackage(aphs)) return aphs } // getPackageQualifiers extracts package qualifiers from affected package data // including CPE information and RPM modularity func getPackageQualifiers(affected models.Affected, cpes any, withCPE bool) *db.PackageQualifiers { var qualifiers *db.PackageQualifiers // Handle CPE qualifiers (existing logic) if withCPE { qualifiers = &db.PackageQualifiers{ PlatformCPEs: cpes.([]string), } } // Extract RPM modularity from ecosystem_specific rpmModularity := extractRpmModularity(affected) if rpmModularity != "" { if qualifiers == nil { qualifiers = &db.PackageQualifiers{} } qualifiers.RpmModularity = &rpmModularity } return qualifiers } // extractRpmModularity extracts RPM modularity information from affected package ecosystem_specific func extractRpmModularity(affected models.Affected) string { if affected.EcosystemSpecific == nil { return "" } rpmModularity, ok := affected.EcosystemSpecific["rpm_modularity"] if !ok { return "" } rpmModularityStr, ok := rpmModularity.(string) if !ok { return "" } return rpmModularityStr } // OSV supports flattered ranges, so both formats below are valid: // "ranges": [ // // { // "type": "SEMVER", // "events": [ // { // "introduced": "12.0.0" // }, // { // "fixed": "12.18.4" // } // ] // }, // { // "type": "SEMVER", // "events": [ // { // "introduced": "14.0.0" // }, // { // "fixed": "14.11.0" // } // ] // } // // ] // "ranges": [ // // { // "type": "SEMVER", // "events": [ // { // "introduced": "12.0.0" // }, // { // "fixed": "12.18.4" // }, // { // "introduced": "14.0.0" // }, // { // "fixed": "14.11.0" // } // ] // } // // ] func getGrypeRangesFromRange(r models.Range, ecosystem string) []db.Range { // nolint: gocognit,funlen var ranges []db.Range if len(r.Events) == 0 { return nil } var constraint string updateConstraint := func(c string) { if constraint == "" { constraint = c } else { constraint = versionutil.AndConstraints(constraint, c) } } fixByVersion := make(map[string]db.FixAvailability) // check r.DatabaseSpecific for "anchore" key which has // {"fixes": [{ // "version": "v1.2.3", // "date": "YYYY-MM-DD", // "kind": "first-observed", // }]} if dbSpecific, ok := r.DatabaseSpecific["anchore"]; ok { if anchoreInfo, ok := dbSpecific.(map[string]any); ok { if fixes, ok := anchoreInfo["fixes"]; ok { if fixList, ok := fixes.([]any); ok { for _, fixEntry := range fixList { if fixMap, ok := fixEntry.(map[string]any); ok { version, vOk := fixMap["version"].(string) kind, kOk := fixMap["kind"].(string) date, dOk := fixMap["date"].(string) if vOk && kOk && dOk { fixByVersion[version] = db.FixAvailability{ Date: internal.ParseTime(date), Kind: kind, } } } } } } } } rangeType := normalizeRangeType(r.Type, ecosystem) for _, e := range r.Events { switch { case e.Introduced != "" && e.Introduced != "0": constraint = fmt.Sprintf(">= %s", e.Introduced) case e.LastAffected != "": updateConstraint(fmt.Sprintf("<= %s", e.LastAffected)) // We don't know the fix if last affected is set ranges = append(ranges, db.Range{ Version: db.Version{ Type: rangeType, Constraint: normalizeConstraint(constraint, rangeType), }, }) // Reset the constraint constraint = "" case e.Fixed != "": var detail *db.FixDetail if f, ok := fixByVersion[e.Fixed]; ok { detail = &db.FixDetail{ Available: &f, } } updateConstraint(fmt.Sprintf("< %s", e.Fixed)) ranges = append(ranges, db.Range{ Fix: normalizeFix(e.Fixed, detail), Version: db.Version{ Type: rangeType, Constraint: normalizeConstraint(constraint, rangeType), }, }) // Reset the constraint constraint = "" } } // Check if there's an event that "introduced" but never had a "fixed" or "last affected" event if constraint != "" { ranges = append(ranges, db.Range{ Version: db.Version{ Type: rangeType, Constraint: normalizeConstraint(constraint, rangeType), }, }) } return ranges } func normalizeConstraint(constraint string, rangeType string) string { if rangeType == "semver" || rangeType == "bitnami" { return versionutil.EnforceSemVerConstraint(constraint) } return constraint } func normalizeFix(fix string, detail *db.FixDetail) *db.Fix { fixedInVersion := versionutil.CleanFixedInVersion(fix) fixState := db.NotFixedStatus if len(fixedInVersion) > 0 { fixState = db.FixedStatus } return &db.Fix{ State: fixState, Version: fixedInVersion, Detail: detail, } } func normalizeRangeType(t models.RangeType, ecosystem string) string { // For Bitnami ecosystem, use "bitnami" format instead of "semver" if ecosystem == "Bitnami" && t == models.RangeSemVer { return "bitnami" } switch t { case models.RangeSemVer, models.RangeEcosystem, models.RangeGit: return strings.ToLower(string(t)) default: return "unknown" } } func getPackage(p models.Package) *db.Package { // Try to determine package type from ecosystem or PURL var pkgType pkg.Type var ecosystem string if p.Purl != "" { pkgType = pkg.TypeFromPURL(p.Purl) ecosystem = string(p.Ecosystem) } else { pkgType = getPackageTypeFromEcosystem(string(p.Ecosystem)) // If we found a package type from OS ecosystem, use it; otherwise use original ecosystem if pkgType != "" { ecosystem = string(pkgType) } else { ecosystem = string(p.Ecosystem) } } return &db.Package{ Ecosystem: ecosystem, Name: name.Normalize(p.Name, pkgType), } } // getPackageTypeFromEcosystem determines package type from OSV ecosystem // Currently only supports AlmaLinux; other ecosystems use PURL-based detection func getPackageTypeFromEcosystem(ecosystem string) pkg.Type { if ecosystem == "" { return "" } // Split ecosystem by colon to get OS name parts := strings.Split(ecosystem, ":") osName := strings.ToLower(parts[0]) // Only handle AlmaLinux if osName == almaLinux { return pkg.RpmPkg } // For other ecosystems (like Bitnami, npm, pypi, etc.), return empty type // The package type will be determined from PURL if available return "" } func getReferences(vuln unmarshal.OSVVulnerability) []db.Reference { var refs []db.Reference for _, ref := range vuln.References { // For advisory references, use the vulnerability ID as the advisory ID // This allows tools consuming the data to link back to the specific advisory refID := "" if ref.Type == models.ReferenceAdvisory && isAdvisoryRecord(vuln) { refID = vuln.ID } refs = append(refs, db.Reference{ ID: refID, URL: ref.URL, Tags: []string{string(ref.Type)}, }, ) } return refs } // extractCVSSInfo extracts the CVSS version and vector from the CVSS string func extractCVSSInfo(cvss string) (string, string, error) { re := regexp.MustCompile(`^CVSS:(\d+\.\d+)/(.+)$`) matches := re.FindStringSubmatch(cvss) if len(matches) != 3 { return "", "", fmt.Errorf("invalid CVSS format") } return matches[1], matches[0], nil } func normalizeSeverity(severity models.Severity) (db.Severity, error) { switch severity.Type { case models.SeverityCVSSV2, models.SeverityCVSSV3, models.SeverityCVSSV4: version, vector, err := extractCVSSInfo(severity.Score) if err != nil { return db.Severity{}, err } return db.Severity{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: vector, Version: version, }, }, nil default: return db.Severity{ Scheme: db.UnknownSeverityScheme, Value: severity.Score, }, nil } } func getSeverities(vuln unmarshal.OSVVulnerability) ([]db.Severity, error) { var severities []db.Severity for _, sev := range vuln.Severity { severity, err := normalizeSeverity(sev) if err != nil { return nil, err } severities = append(severities, severity) } for _, affected := range vuln.Affected { for _, sev := range affected.Severity { severity, err := normalizeSeverity(sev) if err != nil { return nil, err } severities = append(severities, severity) } } return severities, nil } // getOperatingSystemFromEcosystem extracts operating system information from OSV ecosystem field // Currently only supports AlmaLinux ecosystems // Example: "AlmaLinux:8" -> almalinux 8 func getOperatingSystemFromEcosystem(ecosystem string) *db.OperatingSystem { if ecosystem == "" { return nil } // Split ecosystem by colon to get components parts := strings.Split(ecosystem, ":") if len(parts) < 2 { return nil } osName := strings.ToLower(parts[0]) // Only handle AlmaLinux if osName != almaLinux { return nil } osVersion := parts[1] // Parse version into major/minor components versionFields := strings.Split(osVersion, ".") var majorVersion, minorVersion string if len(versionFields) > 0 { majorVersion = versionFields[0] // Check if the first field is actually a number if _, err := strconv.Atoi(majorVersion[0:1]); err != nil { // If not numeric, treat the whole thing as a label version return &db.OperatingSystem{ Name: normalizeOSName(osName), LabelVersion: osVersion, Codename: codename.LookupOS(normalizeOSName(osName), "", ""), } } if len(versionFields) > 1 { minorVersion = versionFields[1] } } return &db.OperatingSystem{ Name: normalizeOSName(osName), MajorVersion: majorVersion, MinorVersion: minorVersion, Codename: codename.LookupOS(normalizeOSName(osName), majorVersion, minorVersion), } } // normalizeOSName normalizes operating system names for consistency // Currently only supports AlmaLinux func normalizeOSName(osName string) string { osName = strings.ToLower(osName) // Only handle AlmaLinux if osName == almaLinux { return almaLinux } return osName } // isAdvisoryRecord checks if the OSV record is marked as an advisory func isAdvisoryRecord(vuln unmarshal.OSVVulnerability) bool { if vuln.DatabaseSpecific == nil { return false } anchoreData, ok := vuln.DatabaseSpecific["anchore"] if !ok { return false } anchoreMap, ok := anchoreData.(map[string]any) if !ok { return false } recordType, ok := anchoreMap["record_type"] if !ok { return false } recordTypeStr, ok := recordType.(string) if !ok { return false } return recordTypeStr == "advisory" } // getUnaffectedPackages creates UnaffectedPackageHandle entries for advisory records func getUnaffectedPackages(vuln unmarshal.OSVVulnerability) []db.UnaffectedPackageHandle { if len(vuln.Affected) == 0 { return nil } var uphs []db.UnaffectedPackageHandle for _, affected := range vuln.Affected { uph := db.UnaffectedPackageHandle{ Package: getPackage(affected.Package), OperatingSystem: getOperatingSystemFromEcosystem(string(affected.Package.Ecosystem)), BlobValue: getUnaffectedBlob(vuln.Aliases, affected.Ranges, affected), } uphs = append(uphs, uph) } // stable ordering sort.Sort(internal.ByUnaffectedPackage(uphs)) return uphs } // getUnaffectedBlob creates a package blob for unaffected packages (advisories) // For advisories, we need to invert the ranges to represent unaffected versions func getUnaffectedBlob(aliases []string, ranges []models.Range, affected models.Affected) *db.PackageBlob { var grypeRanges []db.Range ecosystem := string(affected.Package.Ecosystem) for _, r := range ranges { grypeRanges = append(grypeRanges, getGrypeUnaffectedRangesFromRange(r, ecosystem)...) } // Extract qualifiers including RPM modularity qualifiers := getPackageQualifiers(affected, nil, false) return &db.PackageBlob{ CVEs: aliases, Ranges: grypeRanges, Qualifiers: qualifiers, } } // getGrypeUnaffectedRangesFromRange converts OSV ranges to unaffected version ranges for unaffected packages // This inverts the logic: instead of "< fix_version" (affected), we create ">= fix_version" (unaffected) func getGrypeUnaffectedRangesFromRange(r models.Range, ecosystem string) []db.Range { if len(r.Events) == 0 { return nil } fixByVersion := extractFixAvailability(r) rangeType := normalizeRangeType(r.Type, ecosystem) return buildUnaffectedRangesFromEvents(r.Events, fixByVersion, rangeType) } // extractFixAvailability extracts fix availability information from DatabaseSpecific func extractFixAvailability(r models.Range) map[string]db.FixAvailability { fixByVersion := make(map[string]db.FixAvailability) dbSpecific, hasDBSpecific := r.DatabaseSpecific["anchore"] if !hasDBSpecific { return fixByVersion } anchoreInfo, isMap := dbSpecific.(map[string]any) if !isMap { return fixByVersion } fixes, hasFixes := anchoreInfo["fixes"] if !hasFixes { return fixByVersion } fixList, isList := fixes.([]any) if !isList { return fixByVersion } for _, fixEntry := range fixList { parseSingleFixEntry(fixEntry, fixByVersion) } return fixByVersion } // parseSingleFixEntry parses a single fix entry and adds it to the fixByVersion map func parseSingleFixEntry(fixEntry any, fixByVersion map[string]db.FixAvailability) { fixMap, isMap := fixEntry.(map[string]any) if !isMap { return } version, vOk := fixMap["version"].(string) kind, kOk := fixMap["kind"].(string) date, dOk := fixMap["date"].(string) if vOk && kOk && dOk { fixByVersion[version] = db.FixAvailability{ Date: internal.ParseTime(date), Kind: kind, } } } // buildUnaffectedRangesFromEvents processes events to create unaffected version ranges func buildUnaffectedRangesFromEvents(events []models.Event, fixByVersion map[string]db.FixAvailability, rangeType string) []db.Range { var ranges []db.Range for _, e := range events { if e.Fixed != "" { unaffectedRange := createUnaffectedRange(e.Fixed, fixByVersion, rangeType) ranges = append(ranges, unaffectedRange) } } return ranges } // createUnaffectedRange creates a single safe range for a fixed version func createUnaffectedRange(fixedVersion string, fixByVersion map[string]db.FixAvailability, rangeType string) db.Range { var detail *db.FixDetail if f, ok := fixByVersion[fixedVersion]; ok { detail = &db.FixDetail{ Available: &f, } } constraint := fmt.Sprintf(">= %s", fixedVersion) return db.Range{ Fix: normalizeFix(fixedVersion, detail), Version: db.Version{ Type: rangeType, Constraint: normalizeConstraint(constraint, rangeType), }, } } ================================================ FILE: grype/db/v6/build/transformers/osv/transform_test.go ================================================ package osv import ( "os" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/osv-scanner/pkg/models" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/provider/unmarshal" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" ) var timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) var listing = provider.File{ Path: "some", Digest: "123456", Algorithm: "sha256", } func inputProviderState() provider.State { return provider.State{ Provider: "osv", Version: 12, Processor: "vunnel@1.2.3", Timestamp: timeVal, Listing: &listing, } } func expectedProvider() *db.Provider { return &db.Provider{ ID: "osv", Version: "12", Processor: "vunnel@1.2.3", DateCaptured: &timeVal, InputDigest: "sha256:123456", } } func timeRef(t time.Time) *time.Time { return &t } func loadFixture(t *testing.T, fixturePath string) []unmarshal.OSVVulnerability { t.Helper() f, err := os.Open(fixturePath) require.NoError(t, err) defer func() { require.NoError(t, f.Close()) }() entries, err := unmarshal.OSVVulnerabilityEntries(f) require.NoError(t, err) return entries } func affectedPkgSlice(a ...db.AffectedPackageHandle) []any { var r []any for _, v := range a { r = append(r, v) } return r } func unaffectedPkgSlice(u ...db.UnaffectedPackageHandle) []any { var r []any for _, v := range u { r = append(r, v) } return r } func TestTransform(t *testing.T) { tests := []struct { name string fixturePath string want []transformers.RelatedEntries }{ { name: "Apache 2020-11984", fixturePath: "testdata/BIT-apache-2020-11984.json", want: []transformers.RelatedEntries{{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "BIT-apache-2020-11984", Status: db.VulnerabilityActive, ProviderID: "osv", Provider: expectedProvider(), ModifiedDate: timeRef(time.Date(2025, time.January, 17, 15, 26, 01, 971000000, time.UTC)), PublishedDate: timeRef(time.Date(2024, time.March, 6, 10, 57, 57, 770000000, time.UTC)), BlobValue: &db.VulnerabilityBlob{ ID: "BIT-apache-2020-11984", Description: "Apache HTTP server 2.4.32 to 2.4.44 mod_proxy_uwsgi info disclosure and possible RCE", References: []db.Reference{{ URL: "http://www.openwall.com/lists/oss-security/2020/08/08/1", Tags: []string{"WEB"}, }, { URL: "http://www.openwall.com/lists/oss-security/2020/08/08/10", Tags: []string{"WEB"}, }}, Aliases: []string{"CVE-2020-11984"}, Severities: []db.Severity{{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Version: "3.1", }, }}, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ Package: &db.Package{ Name: "apache", Ecosystem: "Bitnami", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-11984"}, Ranges: []db.Range{{ Version: db.Version{ Type: "bitnami", Constraint: ">=2.4.32,<=2.4.43", }, }}, }, }, ), }}, }, { name: "Node 2020-8201", fixturePath: "testdata/BIT-node-2020-8201.json", want: []transformers.RelatedEntries{{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "BIT-node-2020-8201", Status: db.VulnerabilityActive, ProviderID: "osv", Provider: expectedProvider(), ModifiedDate: timeRef(time.Date(2024, time.March, 6, 11, 25, 28, 861000000, time.UTC)), PublishedDate: timeRef(time.Date(2024, time.March, 6, 11, 8, 9, 371000000, time.UTC)), BlobValue: &db.VulnerabilityBlob{ ID: "BIT-node-2020-8201", Description: "Node.js < 12.18.4 and < 14.11 can be exploited to perform HTTP desync attacks and deliver malicious payloads to unsuspecting users. The payloads can be crafted by an attacker to hijack user sessions, poison cookies, perform clickjacking, and a multitude of other attacks depending on the architecture of the underlying system. The attack was possible due to a bug in processing of carrier-return symbols in the HTTP header names.", References: []db.Reference{{ URL: "https://nodejs.org/en/blog/vulnerability/september-2020-security-releases/", Tags: []string{"WEB"}, }, { URL: "https://security.gentoo.org/glsa/202101-07", Tags: []string{"WEB"}, }}, Aliases: []string{"CVE-2020-8201"}, Severities: []db.Severity{{ Scheme: db.SeveritySchemeCVSS, Value: db.CVSSSeverity{ Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N", Version: "3.1", }, }}, }, }, Related: affectedPkgSlice( db.AffectedPackageHandle{ Package: &db.Package{ Name: "node", Ecosystem: "Bitnami", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2020-8201"}, Ranges: []db.Range{{ Version: db.Version{ Type: "bitnami", Constraint: ">=12.0.0,<12.18.4", }, Fix: &db.Fix{ Version: "12.18.4", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2020, time.September, 15, 0, 0, 0, 0, time.UTC)), Kind: "first-observed", }, }, }, }, { Version: db.Version{ Type: "bitnami", Constraint: ">=14.0.0,<14.11.0", }, Fix: &db.Fix{ Version: "14.11.0", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2020, time.September, 15, 0, 0, 0, 0, time.UTC)), Kind: "first-observed", }, }, }, }}, }, }, ), }}, }, { name: "AlmaLinux Advisory", fixturePath: "testdata/ALSA-2025-7467.json", want: []transformers.RelatedEntries{{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "ALSA-2025:7467", Status: db.VulnerabilityActive, ProviderID: "osv", Provider: expectedProvider(), ModifiedDate: timeRef(time.Date(2025, time.July, 2, 12, 50, 6, 0, time.UTC)), PublishedDate: timeRef(time.Date(2025, time.May, 13, 0, 0, 0, 0, time.UTC)), BlobValue: &db.VulnerabilityBlob{ ID: "ALSA-2025:7467", Description: "The skopeo command lets you inspect images from container image registries.", References: []db.Reference{{ ID: "ALSA-2025:7467", URL: "https://errata.almalinux.org/10/ALSA-2025-7467.html", Tags: []string{"ADVISORY"}, }}, Aliases: []string{"CVE-2025-27144"}, Severities: nil, }, }, Related: unaffectedPkgSlice( db.UnaffectedPackageHandle{ Package: &db.Package{ Name: "skopeo", Ecosystem: "rpm", }, OperatingSystem: &db.OperatingSystem{ Name: "almalinux", MajorVersion: "10", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2025-27144"}, Ranges: []db.Range{{ Version: db.Version{ Type: "ecosystem", Constraint: ">= 2:1.18.1-1.el10_0", }, Fix: &db.Fix{ Version: "2:1.18.1-1.el10_0", State: db.FixedStatus, }, }}, }, }, db.UnaffectedPackageHandle{ Package: &db.Package{ Name: "skopeo-tests", Ecosystem: "rpm", }, OperatingSystem: &db.OperatingSystem{ Name: "almalinux", MajorVersion: "10", }, BlobValue: &db.PackageBlob{ CVEs: []string{"CVE-2025-27144"}, Ranges: []db.Range{{ Version: db.Version{ Type: "ecosystem", Constraint: ">= 2:1.18.1-1.el10_0", }, Fix: &db.Fix{ Version: "2:1.18.1-1.el10_0", State: db.FixedStatus, }, }}, }, }, ), }}, }, } t.Parallel() for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() vulns := loadFixture(t, test.fixturePath) var actual []transformers.RelatedEntries for _, vuln := range vulns { entries, err := Transform(vuln, inputProviderState()) require.NoError(t, err) for _, entry := range entries { e, ok := entry.Data.(transformers.RelatedEntries) require.True(t, ok) actual = append(actual, e) } } if diff := cmp.Diff(test.want, actual); diff != "" { t.Errorf("data entries mismatch (-want +got):\n%s", diff) } }) } } func Test_getGrypeRangesFromRange(t *testing.T) { tests := []struct { name string rnge models.Range ecosystem string want []db.Range }{ { name: "single range with 'fixed' status", ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ Introduced: "0.0.1", }, { Fixed: "0.0.5", }}, }, want: []db.Range{{ Version: db.Version{ Type: "semver", Constraint: ">=0.0.1,<0.0.5", }, Fix: &db.Fix{ Version: "0.0.5", State: db.FixedStatus, }, }}, }, { name: "single range with 'last affected' status", ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ Introduced: "0.0.1", }, { LastAffected: "0.0.5", }}, }, want: []db.Range{{ Version: db.Version{ Type: "semver", Constraint: ">=0.0.1,<=0.0.5", }, }}, }, { name: "single range with no 'fixed' or 'last affected' status", ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ Introduced: "0.0.1", }}, }, want: []db.Range{{ Version: db.Version{ Type: "semver", Constraint: ">=0.0.1", }, }}, }, { name: "single range introduced with '0'", ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ Introduced: "0", }, { LastAffected: "0.0.5", }}, }, want: []db.Range{{ Version: db.Version{ Type: "semver", Constraint: "<=0.0.5", }, }}, }, { name: "multiple ranges", ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ Introduced: "0.0.1", }, { Fixed: "0.0.5", }, { Introduced: "1.0.1", }, { Fixed: "1.0.5", }}, }, want: []db.Range{{ Version: db.Version{ Type: "semver", Constraint: ">=0.0.1,<0.0.5", }, Fix: &db.Fix{ Version: "0.0.5", State: db.FixedStatus, }, }, { Version: db.Version{ Type: "semver", Constraint: ">=1.0.1,<1.0.5", }, Fix: &db.Fix{ Version: "1.0.5", State: db.FixedStatus, }, }, }, }, { name: "single range with database-specific fix availability", ecosystem: "npm", rnge: models.Range{ Type: models.RangeSemVer, Events: []models.Event{{ Introduced: "1.0.0", }, { Fixed: "1.2.3", }}, DatabaseSpecific: map[string]interface{}{ "anchore": map[string]interface{}{ "fixes": []interface{}{ map[string]interface{}{ "version": "1.2.3", "date": "2023-06-15", "kind": "first-observed", }, }, }, }, }, want: []db.Range{{ Version: db.Version{ Type: "semver", Constraint: ">=1.0.0,<1.2.3", }, Fix: &db.Fix{ Version: "1.2.3", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: timeRef(time.Date(2023, time.June, 15, 0, 0, 0, 0, time.UTC)), Kind: "first-observed", }, }, }, }}, }, } t.Parallel() for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() if got := getGrypeRangesFromRange(test.rnge, test.ecosystem); !reflect.DeepEqual(got, test.want) { t.Errorf("getGrypeRangesFromRange() = %v, want %v", got, test.want) } }) } } func Test_getPackage(t *testing.T) { tests := []struct { name string pkg models.Package want *db.Package }{ { name: "valid package", pkg: models.Package{ Ecosystem: "Bitnami", Name: "apache", Purl: "pkg:bitnami/apache", }, want: &db.Package{ Name: "apache", Ecosystem: "Bitnami", }, }, { name: "package with empty purl", pkg: models.Package{ Ecosystem: "Bitnami", Name: "apache", Purl: "", }, want: &db.Package{ Name: "apache", Ecosystem: "Bitnami", }, }, { name: "package with empty ecosystem", pkg: models.Package{ Ecosystem: "", Name: "apache", Purl: "pkg:bitnami/apache", }, want: &db.Package{ Name: "apache", Ecosystem: "", }, }, } t.Parallel() for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() got := getPackage(test.pkg) if got.Name != test.want.Name { t.Errorf("getPackage() got name = %v, want %v", got.Name, test.want.Name) } if got.Ecosystem != test.want.Ecosystem { t.Errorf("getPackage() got ecosystem = %v, want %v", got.Ecosystem, test.want.Ecosystem) } }) } } func Test_extractCVSSInfo(t *testing.T) { tests := []struct { name string cvss string wantVersion string wantVector string wantErr bool }{ { name: "valid cvss", cvss: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", wantVersion: "3.1", wantVector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", wantErr: false, }, { name: "invalid cvss", cvss: "foo:3.1/bar", wantVersion: "", wantVector: "", wantErr: true, }, { name: "empty cvss", cvss: "", wantVersion: "", wantVector: "", wantErr: true, }, { name: "invalid cvss version", cvss: "CVSS:foo/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", wantVersion: "", wantVector: "", wantErr: true, }, } t.Parallel() for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() gotVersion, gotVector, err := extractCVSSInfo(test.cvss) if (err != nil) != test.wantErr { t.Errorf("extractCVSSInfo() error = %v, wantErr %v", err, test.wantErr) return } if gotVersion != test.wantVersion { t.Errorf("extractCVSSInfo() got version = %v, want %v", gotVersion, test.wantVersion) } if gotVector != test.wantVector { t.Errorf("extractCVSSInfo() got vector = %v, want %v", gotVector, test.wantVector) } }) } } func Test_extractRpmModularity(t *testing.T) { tests := []struct { name string affected models.Affected want string }{ { name: "with rpm_modularity", affected: models.Affected{ EcosystemSpecific: map[string]interface{}{ "rpm_modularity": "mariadb:10.3", }, }, want: "mariadb:10.3", }, { name: "no ecosystem_specific", affected: models.Affected{ EcosystemSpecific: nil, }, want: "", }, { name: "no rpm_modularity key", affected: models.Affected{ EcosystemSpecific: map[string]interface{}{ "other_key": "some_value", }, }, want: "", }, { name: "rpm_modularity not string", affected: models.Affected{ EcosystemSpecific: map[string]interface{}{ "rpm_modularity": 123, }, }, want: "", }, { name: "nodejs modularity", affected: models.Affected{ EcosystemSpecific: map[string]interface{}{ "rpm_modularity": "nodejs:16", }, }, want: "nodejs:16", }, } for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { got := extractRpmModularity(test.affected) if got != test.want { t.Errorf("extractRpmModularity() = %v, want %v", got, test.want) } }) } } func Test_getPackageQualifiers(t *testing.T) { tests := []struct { name string affected models.Affected cpes any withCPE bool want *db.PackageQualifiers }{ { name: "with rpm_modularity only", affected: models.Affected{ EcosystemSpecific: map[string]interface{}{ "rpm_modularity": "mariadb:10.3", }, }, cpes: nil, withCPE: false, want: &db.PackageQualifiers{ RpmModularity: stringRef("mariadb:10.3"), }, }, { name: "with CPE only", affected: models.Affected{ EcosystemSpecific: nil, }, cpes: []string{"cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*"}, withCPE: true, want: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*"}, }, }, { name: "with both rpm_modularity and CPE", affected: models.Affected{ EcosystemSpecific: map[string]interface{}{ "rpm_modularity": "nodejs:16", }, }, cpes: []string{"cpe:2.3:a:nodejs:nodejs:*:*:*:*:*:*:*:*"}, withCPE: true, want: &db.PackageQualifiers{ PlatformCPEs: []string{"cpe:2.3:a:nodejs:nodejs:*:*:*:*:*:*:*:*"}, RpmModularity: stringRef("nodejs:16"), }, }, { name: "no qualifiers", affected: models.Affected{ EcosystemSpecific: nil, }, cpes: nil, withCPE: false, want: nil, }, } for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { got := getPackageQualifiers(test.affected, test.cpes, test.withCPE) if !reflect.DeepEqual(got, test.want) { t.Errorf("getPackageQualifiers() = %v, want %v", got, test.want) } }) } } func stringRef(s string) *string { return &s } ================================================ FILE: grype/db/v6/build/transformers/references.go ================================================ package transformers import db "github.com/anchore/grype/grype/db/v6" // DeduplicateReferences removes duplicate references, where two references are considered // identical if they have the same URL and their normalized, sorted tags are equal func DeduplicateReferences(references []db.Reference) []db.Reference { var result []db.Reference seenBefore := make(map[string][]db.Reference) for _, ref := range references { if _, anySeenRefs := seenBefore[ref.URL]; !anySeenRefs { seenBefore[ref.URL] = []db.Reference{ref} result = append(result, ref) continue } alreadySeenRefs := seenBefore[ref.URL] isDuplicate := false // Check if this reference already exists for this URL for _, already := range alreadySeenRefs { if refsAreEqual(already, ref) { isDuplicate = true break } } if !isDuplicate { seenBefore[ref.URL] = append(seenBefore[ref.URL], ref) result = append(result, ref) } } return result } func refsAreEqual(a, b db.Reference) bool { if a.URL != b.URL { return false } if len(a.Tags) != len(b.Tags) { return false } for i := range a.Tags { if a.Tags[i] != b.Tags[i] { return false } } return true } ================================================ FILE: grype/db/v6/build/writer.go ================================================ package v6 import ( "fmt" "strings" "sync" "time" "github.com/anchore/go-logger" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" "github.com/anchore/grype/internal/log" ) var _ data.Writer = (*writer)(nil) type writer struct { dbPath string failOnMissingFixDate bool store db.ReadWriter providerCache map[string]db.Provider states provider.States severityCache map[string]db.Severity // Two-tier batching: parent records (vulnerabilities + providers) and child records (related entries) // This maintains FK integrity while maximizing batch sizes parentBatchSize int childBatchSize int parentBuffer []func() error childBuffer []func() error mu sync.Mutex // Protect batch state // Metrics totalParentBatches int totalChildBatches int } type ProviderMetadata struct { Providers []Provider `json:"providers"` } type Provider struct { Name string `json:"name"` LastSuccessfulRun time.Time `json:"lastSuccessfulRun"` } func NewWriter(directory string, states provider.States, failOnMissingFixDate bool, batchSize int) (data.Writer, error) { cfg := db.Config{ DBDirPath: directory, } s, err := db.NewWriter(cfg) if err != nil { return nil, fmt.Errorf("unable to create store: %w", err) } if err := s.SetDBMetadata(); err != nil { return nil, fmt.Errorf("unable to set DB ID: %w", err) } // Use default if not configured if batchSize == 0 { batchSize = 2000 } return &writer{ dbPath: cfg.DBFilePath(), failOnMissingFixDate: failOnMissingFixDate, providerCache: make(map[string]db.Provider), store: s, states: states, severityCache: make(map[string]db.Severity), parentBatchSize: batchSize, childBatchSize: batchSize, parentBuffer: make([]func() error, 0, batchSize), childBuffer: make([]func() error, 0, batchSize), }, nil } func (w *writer) Write(entries ...data.Entry) error { for _, entry := range entries { if entry.DBSchemaVersion != db.ModelVersion { return fmt.Errorf("wrong schema version: want %+v got %+v", db.ModelVersion, entry.DBSchemaVersion) } switch row := entry.Data.(type) { case transformers.RelatedEntries: if err := w.writeEntry(row); err != nil { return fmt.Errorf("unable to write entry to store: %w", err) } default: return fmt.Errorf("data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T", row) } } return nil } func (w *writer) writeEntry(entry transformers.RelatedEntries) error { log.WithFields("entry", entry.String()).Trace("writing entry") if entry.VulnerabilityHandle != nil { w.fillInMissingSeverity(entry.VulnerabilityHandle) // Add vulnerability to parent batch // CRITICAL: Use pointer directly (not copy) so ID assignment propagates to child operations vulnHandle := entry.VulnerabilityHandle if err := w.addToParentBatch(func() error { return w.store.AddVulnerabilities(vulnHandle) }); err != nil { return fmt.Errorf("unable to batch vulnerability write: %w", err) } } // Handle providers for entries without vulnerabilities (EPSS, KEV, etc.) // AddVulnerabilities() only handles providers implicitly for vulnerability entries if entry.Provider != nil && entry.VulnerabilityHandle == nil { provider := *entry.Provider if err := w.addToParentBatch(func() error { return w.store.AddProvider(provider) }); err != nil { return fmt.Errorf("unable to batch provider write: %w", err) } } // Add all related entries to child batch // NOTE: No explicit flush here. Parent batch auto-flushes at threshold. // Child batch auto-flush will flush parent first to maintain FK integrity. for i := range entry.Related { if err := w.writeRelatedEntry(entry.VulnerabilityHandle, entry.Related[i]); err != nil { return err } } return nil } func (w *writer) writeRelatedEntry(vulnHandle *db.VulnerabilityHandle, related any) error { switch row := related.(type) { case db.AffectedPackageHandle: return w.writeAffectedPackage(vulnHandle, row) case db.AffectedCPEHandle: return w.writeAffectedCPE(vulnHandle, row) case db.KnownExploitedVulnerabilityHandle: // Add KEV to child batch - copy to avoid pointer reuse kevHandle := row return w.addToChildBatch(func() error { handleCopy := kevHandle return w.store.AddKnownExploitedVulnerabilities(&handleCopy) }) case db.UnaffectedPackageHandle: return w.writeUnaffectedPackage(vulnHandle, row) case db.UnaffectedCPEHandle: return w.writeUnaffectedCPE(vulnHandle, row) case db.EpssHandle: // Add EPSS to child batch - copy to avoid pointer reuse epssHandle := row return w.addToChildBatch(func() error { handleCopy := epssHandle return w.store.AddEpss(&handleCopy) }) case db.CWEHandle: // Add CWE to child batch - copy to avoid pointer reuse cweHandle := row return w.addToChildBatch(func() error { handleCopy := cweHandle return w.store.AddCWE(&handleCopy) }) case db.OperatingSystemEOLHandle: // Add OS EOL to child batch - copy to avoid pointer reuse eolHandle := row return w.addToChildBatch(func() error { handleCopy := eolHandle return w.writeOperatingSystemEOL(handleCopy) }) default: return fmt.Errorf("data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T", row) } } func (w *writer) writeAffectedPackage(vulnHandle *db.VulnerabilityHandle, row db.AffectedPackageHandle) error { if w.failOnMissingFixDate { if err := ensureFixDates(&row); err != nil { fields := logger.Fields{ "pkg": row.Package, } if vulnHandle != nil { fields["vulnerability"] = vulnHandle.Name } if row.BlobValue != nil { fields["ranges"] = row.BlobValue.String() } if row.OperatingSystem != nil { fields["os"] = row.OperatingSystem } log.WithFields(fields).Error("fix date validation failed") return fmt.Errorf("unable to validate fix dates: %w", err) } } // Add affected package to child batch - defer VulnerabilityID assignment until flush pkgHandle := row return w.addToChildBatch(func() error { handleCopy := pkgHandle if vulnHandle != nil { handleCopy.VulnerabilityID = vulnHandle.ID } else { log.WithFields("package", handleCopy.Package).Warn("affected package entry does not have a vulnerability ID") } return w.store.AddAffectedPackages(&handleCopy) }) } func (w *writer) writeAffectedCPE(vulnHandle *db.VulnerabilityHandle, row db.AffectedCPEHandle) error { // Add affected CPE to child batch - defer VulnerabilityID assignment until flush // when the parent vulnerability has been written and ID is assigned cpeHandle := row return w.addToChildBatch(func() error { handleCopy := cpeHandle if vulnHandle != nil { handleCopy.VulnerabilityID = vulnHandle.ID } else { log.WithFields("cpe", handleCopy.CPE).Warn("affected CPE entry does not have a vulnerability ID") } return w.store.AddAffectedCPEs(&handleCopy) }) } func (w *writer) writeUnaffectedPackage(vulnHandle *db.VulnerabilityHandle, row db.UnaffectedPackageHandle) error { // Add unaffected package to child batch - defer VulnerabilityID assignment until flush pkgHandle := row return w.addToChildBatch(func() error { handleCopy := pkgHandle if vulnHandle != nil { handleCopy.VulnerabilityID = vulnHandle.ID } else { log.WithFields("package", handleCopy.Package).Warn("unaffected package entry does not have a vulnerability ID") } return w.store.AddUnaffectedPackages(&handleCopy) }) } func (w *writer) writeUnaffectedCPE(vulnHandle *db.VulnerabilityHandle, row db.UnaffectedCPEHandle) error { // Add unaffected CPE to child batch - defer VulnerabilityID assignment until flush cpeHandle := row return w.addToChildBatch(func() error { handleCopy := cpeHandle if vulnHandle != nil { handleCopy.VulnerabilityID = vulnHandle.ID } else { log.WithFields("cpe", handleCopy.CPE).Warn("unaffected CPE entry does not have a vulnerability ID") } return w.store.AddUnaffectedCPEs(&handleCopy) }) } // fillInMissingSeverity will add a severity entry to the vulnerability record if it is missing, empty, or "unknown". // The upstream NVD record is used to fill in these missing values. Note that the NVD provider is always guaranteed // to be processed first before other providers. func (w *writer) fillInMissingSeverity(handle *db.VulnerabilityHandle) { if handle == nil { return } blob := handle.BlobValue if blob == nil { return } id := strings.ToLower(blob.ID) isCVE := strings.HasPrefix(id, "cve-") if strings.ToLower(handle.ProviderID) == "nvd" && isCVE { if len(blob.Severities) > 0 { w.severityCache[id] = blob.Severities[0] } return } if !isCVE { return } // parse all string severities and remove all unknown values sevs := filterUnknownSeverities(blob.Severities) topSevStr := "none" if len(sevs) > 0 { switch v := sevs[0].Value.(type) { case string: topSevStr = v case fmt.Stringer: topSevStr = v.String() default: topSevStr = fmt.Sprintf("%v", sevs[0].Value) } } if len(sevs) > 0 { return // already has a severity, don't normalize } // add the top NVD severity value nvdSev, ok := w.severityCache[id] if !ok { log.WithFields("id", blob.ID).Trace("unable to find NVD severity") return } log.WithFields("id", blob.ID, "provider", handle.Provider, "sev-from", topSevStr, "sev-to", nvdSev).Trace("overriding irrelevant severity with data from NVD record") sevs = append([]db.Severity{nvdSev}, sevs...) handle.BlobValue.Severities = sevs } // addToParentBatch adds an operation to parent buffer and flushes when threshold reached func (w *writer) addToParentBatch(op func() error) error { w.mu.Lock() defer w.mu.Unlock() w.parentBuffer = append(w.parentBuffer, op) // Flush parent batch when it reaches threshold to limit memory usage if len(w.parentBuffer) >= w.parentBatchSize { return w.flushParentBatchLocked() } return nil } // addToChildBatch adds an operation to child buffer and flushes when threshold reached func (w *writer) addToChildBatch(op func() error) error { w.mu.Lock() defer w.mu.Unlock() w.childBuffer = append(w.childBuffer, op) // When child buffer is full, flush BOTH buffers (parents first for FK integrity) if len(w.childBuffer) >= w.childBatchSize { // Flush parents first to ensure IDs are assigned for children to reference if err := w.flushParentBatchLocked(); err != nil { return err } // Then flush children return w.flushChildBatchLocked() } return nil } // flushParentBatch executes all pending parent operations in batches of parentBatchSize func (w *writer) flushParentBatch() error { w.mu.Lock() defer w.mu.Unlock() return w.flushParentBatchLocked() } // flushParentBatchLocked executes all pending parent operations (must be called with lock held) func (w *writer) flushParentBatchLocked() error { if len(w.parentBuffer) == 0 { return nil } log.WithFields("total_operations", len(w.parentBuffer), "batch_size", w.parentBatchSize).Debug("flushing parent operations") // Execute all accumulated operations for j, op := range w.parentBuffer { if err := op(); err != nil { return fmt.Errorf("parent operation %d failed: %w", j, err) } } w.totalParentBatches++ w.parentBuffer = w.parentBuffer[:0] return nil } // flushChildBatch executes all pending child operations in batches of childBatchSize func (w *writer) flushChildBatch() error { w.mu.Lock() defer w.mu.Unlock() return w.flushChildBatchLocked() } // flushChildBatchLocked executes all pending child operations (must be called with lock held) func (w *writer) flushChildBatchLocked() error { if len(w.childBuffer) == 0 { return nil } log.WithFields("total_operations", len(w.childBuffer), "batch_size", w.childBatchSize).Debug("flushing child operations") // Execute all accumulated operations for j, op := range w.childBuffer { if err := op(); err != nil { return fmt.Errorf("child operation %d failed: %w", j, err) } } w.totalChildBatches++ w.childBuffer = w.childBuffer[:0] return nil } func (w *writer) Close() error { // Flush any remaining batched operations (both parent and child) if err := w.flushParentBatch(); err != nil { return fmt.Errorf("unable to flush parent batch: %w", err) } if err := w.flushChildBatch(); err != nil { return fmt.Errorf("unable to flush child batch: %w", err) } if err := w.store.Close(); err != nil { return fmt.Errorf("unable to close store: %w", err) } log.WithFields( "path", w.dbPath, "parent_batches", w.totalParentBatches, "child_batches", w.totalChildBatches, ).Info("database created") return nil } func filterUnknownSeverities(sevs []db.Severity) []db.Severity { var out []db.Severity for _, s := range sevs { if isKnownSeverity(s) { out = append(out, s) } } return out } func isKnownSeverity(s db.Severity) bool { switch v := s.Value.(type) { case string: return v != "" && strings.ToLower(v) != "unknown" default: return v != nil } } func ensureFixDates(row *db.AffectedPackageHandle) error { if row.BlobValue == nil { return nil } for _, r := range row.BlobValue.Ranges { if r.Fix == nil { continue } if !isFixVersion(r.Fix.Version) || r.Fix.State != db.FixedStatus { continue } if r.Fix.Detail == nil || r.Fix.Detail.Available == nil || r.Fix.Detail.Available.Date == nil { return fmt.Errorf("missing fix date for version %q", r.Fix.Version) } if r.Fix.Detail.Available.Date.IsZero() { return fmt.Errorf("zero fix date for version %q", r.Fix.Version) } } return nil } func isFixVersion(v string) bool { return v != "" && v != "0" && strings.ToLower(v) != "none" } func (w *writer) writeOperatingSystemEOL(row db.OperatingSystemEOLHandle) error { spec := db.OSSpecifier{ Name: row.Name, MajorVersion: row.MajorVersion, MinorVersion: row.MinorVersion, LabelVersion: row.Codename, } updated, err := w.store.UpdateOperatingSystemEOL(spec, row.EOLDate, row.EOASDate) if err != nil { return fmt.Errorf("unable to update OS EOL data: %w", err) } if updated == 0 { log.WithFields("os", row.String()).Trace("no OS record found to update with EOL data") } return nil } ================================================ FILE: grype/db/v6/build/writer_test.go ================================================ package v6 import ( "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/data" "github.com/anchore/grype/grype/db/provider" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/build/transformers" ) func TestFillInMissingSeverity(t *testing.T) { tests := []struct { name string handle *db.VulnerabilityHandle severityCache map[string]db.Severity expected []db.Severity expectCacheUpdate bool }{ { name: "nil handle", handle: nil, severityCache: map[string]db.Severity{}, expected: nil, }, { name: "nil metadata", handle: &db.VulnerabilityHandle{ BlobValue: nil, }, severityCache: map[string]db.Severity{}, expected: nil, }, { name: "non-CVE ID", handle: &db.VulnerabilityHandle{ BlobValue: &db.VulnerabilityBlob{ ID: "GHSA-123", Severities: []db.Severity{ {Value: "high"}, }, }, }, severityCache: map[string]db.Severity{}, expected: []db.Severity{{Value: "high"}}, }, { name: "NVD provider with CVE", handle: &db.VulnerabilityHandle{ ProviderID: "nvd", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-1234", Severities: []db.Severity{ {Value: "critical"}, }, }, }, severityCache: map[string]db.Severity{}, expected: []db.Severity{{Value: "critical"}}, expectCacheUpdate: true, }, { name: "CVE with existing severities", handle: &db.VulnerabilityHandle{ ProviderID: "github", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-5678", Severities: []db.Severity{ {Value: "medium"}, {Value: "high"}, }, }, }, severityCache: map[string]db.Severity{ "cve-2023-5678": {Value: "critical"}, }, expected: []db.Severity{ {Value: "medium"}, {Value: "high"}, }, }, { name: "CVE with no severities, using cache", handle: &db.VulnerabilityHandle{ ProviderID: "github", BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-9012", Severities: []db.Severity{}, }, }, severityCache: map[string]db.Severity{ "cve-2023-9012": {Value: "high"}, }, expected: []db.Severity{{Value: "high"}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &writer{ severityCache: tt.severityCache, } if tt.expectCacheUpdate { // assert expected ids are not in the cache if tt.handle != nil && tt.handle.BlobValue != nil { assert.NotContains(t, tt.severityCache, strings.ToLower(tt.handle.BlobValue.ID)) } } w.fillInMissingSeverity(tt.handle) if tt.handle == nil || tt.handle.BlobValue == nil { return } if tt.expectCacheUpdate { // assert expected ids are not in the cache if tt.handle != nil && tt.handle.BlobValue != nil { id := strings.ToLower(tt.handle.BlobValue.ID) assert.Equal(t, tt.severityCache[id], w.severityCache[id]) } } assert.Equal(t, tt.expected, tt.handle.BlobValue.Severities) }) } } func TestFilterUnknownSeverities(t *testing.T) { tests := []struct { name string input []db.Severity expected []db.Severity }{ { name: "empty input", input: []db.Severity{}, expected: nil, }, { name: "all known severities", input: []db.Severity{ {Value: "critical"}, {Value: "high"}, {Value: "medium"}, }, expected: []db.Severity{ {Value: "critical"}, {Value: "high"}, {Value: "medium"}, }, }, { name: "mix of known and unknown", input: []db.Severity{ {Value: "high"}, {Value: "unknown"}, {Value: "medium"}, {Value: ""}, }, expected: []db.Severity{ {Value: "high"}, {Value: "medium"}, }, }, { name: "non-string values", input: []db.Severity{ {Value: 5}, {Value: nil}, {Value: "high"}, }, expected: []db.Severity{ {Value: 5}, {Value: "high"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterUnknownSeverities(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsKnownSeverity(t *testing.T) { tests := []struct { name string severity db.Severity expected bool }{ { name: "empty string", severity: db.Severity{Value: ""}, expected: false, }, { name: "unknown string", severity: db.Severity{Value: "unknown"}, expected: false, }, { name: "case insensitive", severity: db.Severity{Value: "UNKNOWN"}, expected: false, }, { name: "valid string severity", severity: db.Severity{Value: "high"}, expected: true, }, { name: "nil value", severity: db.Severity{Value: nil}, expected: false, }, { name: "numeric value", severity: db.Severity{Value: 7}, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isKnownSeverity(tt.severity) assert.Equal(t, tt.expected, result) }) } } func TestEnsureFixDates(t *testing.T) { validDate := time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC) zeroDate := time.Time{} tests := []struct { name string row *db.AffectedPackageHandle wantErr require.ErrorAssertionFunc }{ { name: "nil BlobValue", row: &db.AffectedPackageHandle{ BlobValue: nil, }, }, { name: "empty ranges", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{}, }, }, }, { name: "range with nil Fix", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ {Fix: nil}, }, }, }, }, { name: "range with empty Fix.Version", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ {Fix: &db.Fix{Version: ""}}, }, }, }, }, { name: "range with Fix.Version '0' - skipped by isFixVersion", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "0", // invalid version - validation skipped State: db.FixedStatus, Detail: nil, // no date but should not error }, }, }, }, }, }, { name: "range with Fix.Version 'none' - skipped by isFixVersion", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "none", // invalid version - validation skipped State: db.FixedStatus, Detail: nil, // no date but should not error }, }, }, }, }, }, { name: "range with Fix.Version 'NONE' (case insensitive) - skipped by isFixVersion", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "NONE", // invalid version - validation skipped State: db.FixedStatus, Detail: nil, // no date but should not error }, }, }, }, }, }, { name: "range with Fix.State not FixedStatus", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", State: db.NotAffectedFixStatus, }, }, }, }, }, }, { name: "valid fix with proper date", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: &validDate, }, }, }, }, }, }, }, }, { name: "valid version requires date validation", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", // valid version - validation required State: db.FixedStatus, Detail: nil, // no date should cause error }, }, }, }, }, wantErr: require.Error, }, { name: "multiple ranges with valid dates", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: &validDate, }, }, }, }, { Fix: &db.Fix{ Version: "2.0.0", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: &validDate, }, }, }, }, }, }, }, }, { name: "mix of valid and nil Fix ranges", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ {Fix: nil}, { Fix: &db.Fix{ Version: "1.2.3", State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: &validDate, }, }, }, }, {Fix: &db.Fix{Version: ""}}, }, }, }, }, { name: "missing Fix.Detail with valid version", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", // valid version triggers validation State: db.FixedStatus, Detail: nil, }, }, }, }, }, wantErr: require.Error, }, { name: "missing Fix.Detail.Available with valid version", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "2.0.0", // valid version triggers validation State: db.FixedStatus, Detail: &db.FixDetail{ Available: nil, }, }, }, }, }, }, wantErr: require.Error, }, { name: "missing Fix.Detail.Available.Date with valid version", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "v1.0.0", // valid version triggers validation State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: nil, }, }, }, }, }, }, }, wantErr: require.Error, }, { name: "zero Fix.Detail.Available.Date with valid version", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "3.1.4", // valid version triggers validation State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: &zeroDate, }, }, }, }, }, }, }, wantErr: require.Error, }, { name: "multiple ranges with one missing date and valid versions", row: &db.AffectedPackageHandle{ BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", // valid version triggers validation State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: &validDate, }, }, }, }, { Fix: &db.Fix{ Version: "2.0.0", // valid version triggers validation State: db.FixedStatus, Detail: &db.FixDetail{ Available: &db.FixAvailability{ Date: nil, }, }, }, }, }, }, }, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } err := ensureFixDates(tt.row) tt.wantErr(t, err) }) } } func TestWrite_FailsOnMissingFixDate(t *testing.T) { // test proves that Write() method errors out when fix date validation is enabled // and a fix is marked as FixedStatus but lacks the required date information w := &writer{ failOnMissingFixDate: true, store: nil, // intentionally nil - we should error before reaching store operations severityCache: make(map[string]db.Severity), } var vulnID db.ID = 123 entry := data.Entry{ DBSchemaVersion: db.ModelVersion, Data: transformers.RelatedEntries{ VulnerabilityHandle: nil, // no vulnerability handle to avoid store operations Related: []any{ db.AffectedPackageHandle{ VulnerabilityID: vulnID, Package: &db.Package{Name: "test-package"}, BlobValue: &db.PackageBlob{ Ranges: []db.Range{ { Fix: &db.Fix{ Version: "1.2.3", // valid version triggers validation State: db.FixedStatus, Detail: nil, // missing fix detail should cause error }, }, }, }, }, }, }, } err := w.Write(entry) require.Error(t, err) require.Contains(t, err.Error(), "unable to validate fix dates") require.Contains(t, err.Error(), "missing fix date for version \"1.2.3\"") } func TestIsFixVersion(t *testing.T) { tests := []struct { name string version string expected bool }{ { name: "empty string", version: "", expected: false, }, { name: "zero version", version: "0", expected: false, }, { name: "none lowercase", version: "none", expected: false, }, { name: "none uppercase", version: "NONE", expected: false, }, { name: "none mixed case", version: "None", expected: false, }, { name: "valid semantic version", version: "1.2.3", expected: true, }, { name: "valid version with prefix", version: "v1.2.3", expected: true, }, { name: "valid version with patch level", version: "2.4.1-rc1", expected: true, }, { name: "valid commit hash", version: "abc123def", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isFixVersion(tt.version) require.Equal(t, tt.expected, result) }) } } func TestBatchedWritesEquivalence(t *testing.T) { // Test that batched writes produce identical database output to unbatched writes // This is the critical correctness test for the batching optimization testCases := []struct { name string batchSize int numEntries int }{ { name: "unbatched (batch_size=1)", batchSize: 1, numEntries: 50, }, { name: "small batch", batchSize: 10, numEntries: 50, }, { name: "large batch", batchSize: 2000, numEntries: 50, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create temp directory for database tmpDir := t.TempDir() // Create writer with specified batch size w, err := NewWriter(tmpDir, provider.States{}, false, tc.batchSize) require.NoError(t, err) // Write test entries entries := createTestEntries(tc.numEntries) for _, entry := range entries { err := w.Write(entry) require.NoError(t, err) } // Close to flush all batches err = w.Close() require.NoError(t, err) // Verify database was created dbPath := filepath.Join(tmpDir, "vulnerability.db") _, err = os.Stat(dbPath) require.NoError(t, err, "database file should exist") // Open and verify database contents reader, err := db.NewReader(db.Config{DBDirPath: tmpDir}) require.NoError(t, err) defer reader.Close() // Basic validation: verify we can read data back // More detailed validation would require actual query methods // but this proves the database is valid and readable }) } } func TestBatchAccumulation(t *testing.T) { // Test that operations accumulate in buffers before flushing tmpDir := t.TempDir() w, err := NewWriter(tmpDir, provider.States{}, false, 1000) require.NoError(t, err) writerImpl := w.(*writer) // Write 50 entries (below batch threshold of 1000) entries := createTestEntries(50) for _, entry := range entries { err := writerImpl.Write(entry) require.NoError(t, err) } // Verify buffers contain accumulated operations (not flushed yet) assert.Greater(t, len(writerImpl.parentBuffer), 0, "parent buffer should contain operations") assert.Greater(t, len(writerImpl.childBuffer), 0, "child buffer should contain operations") assert.Equal(t, 0, writerImpl.totalParentBatches, "should not have flushed yet") assert.Equal(t, 0, writerImpl.totalChildBatches, "should not have flushed yet") // Close should flush everything err = writerImpl.Close() require.NoError(t, err) // Verify buffers were flushed assert.Equal(t, 0, len(writerImpl.parentBuffer), "parent buffer should be empty after close") assert.Equal(t, 0, len(writerImpl.childBuffer), "child buffer should be empty after close") assert.Greater(t, writerImpl.totalParentBatches, 0, "should have flushed parent batch") assert.Greater(t, writerImpl.totalChildBatches, 0, "should have flushed child batch") } func TestBatchMetrics(t *testing.T) { // Test that batch counts accurately reflect number of flushes tmpDir := t.TempDir() batchSize := 25 numEntries := 100 w, err := NewWriter(tmpDir, provider.States{}, false, batchSize) require.NoError(t, err) writerImpl := w.(*writer) // Write entries entries := createTestEntries(numEntries) for _, entry := range entries { err := writerImpl.Write(entry) require.NoError(t, err) } err = writerImpl.Close() require.NoError(t, err) // Verify batch counts // With 100 entries, batchSize=25: // - Parent ops: 100 vulnerabilities / 25 = 4 batches // - Child ops: depends on children per entry, but should also batch assert.Greater(t, writerImpl.totalParentBatches, 0, "should have parent batches") assert.Greater(t, writerImpl.totalChildBatches, 0, "should have child batches") } func TestBatchSizeConfiguration(t *testing.T) { // Test that batch size defaults and configuration work correctly tmpDir := t.TempDir() tests := []struct { name string inputSize int expectedSize int }{ { name: "default (0 -> 2000)", inputSize: 0, expectedSize: 2000, }, { name: "custom size", inputSize: 500, expectedSize: 500, }, { name: "unbatched mode", inputSize: 1, expectedSize: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w, err := NewWriter(tmpDir, provider.States{}, false, tt.inputSize) require.NoError(t, err) defer w.Close() writerImpl := w.(*writer) assert.Equal(t, tt.expectedSize, writerImpl.parentBatchSize) assert.Equal(t, tt.expectedSize, writerImpl.childBatchSize) }) } } // createTestEntries creates test entries with unique identifiable content func createTestEntries(count int) []data.Entry { entries := make([]data.Entry, count) for i := 0; i < count; i++ { entries[i] = data.Entry{ DBSchemaVersion: db.ModelVersion, Data: transformers.RelatedEntries{ VulnerabilityHandle: &db.VulnerabilityHandle{ Name: "CVE-2023-TEST", ProviderID: "test-provider", Provider: &db.Provider{ ID: "test-provider", Version: "1.0.0", }, BlobValue: &db.VulnerabilityBlob{ ID: "CVE-2023-TEST", }, }, Related: []any{ db.CWEHandle{ CVE: "CVE-2023-TEST", CWE: "CWE-79", }, }, }, } } return entries } ================================================ FILE: grype/db/v6/cache.go ================================================ package v6 import ( "context" "sync" ) const ( cpesTableCacheKey = "cpes" packagesTableCacheKey = "packages" operatingSystemsTableCacheKey = "operating_systems" vulnerabilitiesTableCacheKey = "vulnerabilities" ) const cacheKey = contextKey("multiModelCache") type contextKey string type cachable interface { cacheKey() string tableName() string } type cacheIDManager interface { rowID() ID setRowID(ID) } type cacheStringIDManager interface { rowID() string setRowID(string) } func withCacheContext(ctx context.Context, c *cache) context.Context { return context.WithValue(ctx, cacheKey, c) } func cacheFromContext(ctx context.Context) (*cache, bool) { c, ok := ctx.Value(cacheKey).(*cache) return c, ok } type cache struct { mu sync.RWMutex idKeys map[string]map[string]ID strKeys map[string]map[string]string } func newCache() *cache { return &cache{ idKeys: make(map[string]map[string]ID), strKeys: make(map[string]map[string]string), } } func (c *cache) getID(ca cachable) (ID, bool) { c.mu.RLock() defer c.mu.RUnlock() if tableCache, exists := c.idKeys[ca.tableName()]; exists { id, found := tableCache[ca.cacheKey()] return id, found } return 0, false } func (c *cache) getString(ca cachable) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() if tableCache, exists := c.strKeys[ca.tableName()]; exists { id, found := tableCache[ca.cacheKey()] return id, found } return "", false } func (c *cache) set(ca cachable) { switch cam := ca.(type) { case cacheIDManager: c.setIDEntry(cam.rowID(), ca) case cacheStringIDManager: c.setStringEntry(cam.rowID(), ca) default: panic("unsupported cacheable type") } } func (c *cache) setStringEntry(id string, ca cachable) { table := ca.tableName() key := ca.cacheKey() c.mu.Lock() defer c.mu.Unlock() if _, exists := c.strKeys[table]; !exists { c.strKeys[table] = make(map[string]string) } c.strKeys[table][key] = id } func (c *cache) setIDEntry(id ID, ca cachable) { table := ca.tableName() key := ca.cacheKey() c.mu.Lock() defer c.mu.Unlock() if _, exists := c.idKeys[table]; !exists { c.idKeys[table] = make(map[string]ID) } c.idKeys[table][key] = id } ================================================ FILE: grype/db/v6/cache_test.go ================================================ package v6 import ( "context" "testing" "github.com/stretchr/testify/require" ) type mockCachableID struct { key string table string id ID } func (m *mockCachableID) cacheKey() string { return m.key } func (m *mockCachableID) tableName() string { return m.table } func (m *mockCachableID) rowID() ID { return m.id } func (m *mockCachableID) setRowID(id ID) { m.id = id } type mockCachableString struct { key string table string id string } func (m *mockCachableString) cacheKey() string { return m.key } func (m *mockCachableString) tableName() string { return m.table } func (m *mockCachableString) rowID() string { return m.id } func (m *mockCachableString) setRowID(id string) { m.id = id } func newTestCachableString(key, table, id string) *mockCachableString { return &mockCachableString{key: key, table: table, id: id} } func newTestCachableID(key, table string, id ID) *mockCachableID { return &mockCachableID{key: key, table: table, id: id} } func TestCache_GetString_Found(t *testing.T) { c := newCache() item := newTestCachableString("test-key", "test-table", "test-id") c.setStringEntry("test-id", item) str, found := c.getString(item) require.True(t, found) require.Equal(t, "test-id", str) } func TestCache_GetString_NotFound(t *testing.T) { c := newCache() item := newTestCachableString("missing-key", "test-table", "") str, found := c.getString(item) require.False(t, found) require.Empty(t, str) } func TestCache_Set_ID(t *testing.T) { c := newCache() item := newTestCachableID("test-key", "test-table", 123) c.set(item) id, found := c.getID(item) require.True(t, found) require.Equal(t, ID(123), id) } func TestCache_Set_String(t *testing.T) { c := newCache() item := newTestCachableString("test-key", "test-table", "test-id") c.set(item) str, found := c.getString(item) require.True(t, found) require.Equal(t, "test-id", str) } func TestCache_Set_Panic(t *testing.T) { c := newCache() invalidItem := struct{ cachable }{} require.PanicsWithValue(t, "unsupported cacheable type", func() { c.set(invalidItem) }) } func TestCache_SetStringEntry_New(t *testing.T) { c := newCache() item := newTestCachableString("test-key", "test-table", "") c.setStringEntry("new-id", item) str, found := c.getString(item) require.True(t, found) require.Equal(t, "new-id", str) } func TestCache_SetStringEntry_Update(t *testing.T) { c := newCache() item := newTestCachableString("test-key", "test-table", "old-id") c.setStringEntry("old-id", item) c.setStringEntry("new-id", item) str, found := c.getString(item) require.True(t, found) require.Equal(t, "new-id", str) } func TestCache_SetIDEntry_New(t *testing.T) { c := newCache() item := newTestCachableID("test-key", "test-table", 0) c.setIDEntry(123, item) id, found := c.getID(item) require.True(t, found) require.Equal(t, ID(123), id) } func TestCache_SetIDEntry_Update(t *testing.T) { c := newCache() item := newTestCachableID("test-key", "test-table", 123) c.setIDEntry(123, item) c.setIDEntry(456, item) id, found := c.getID(item) require.True(t, found) require.Equal(t, ID(456), id) } func TestWithCacheContext(t *testing.T) { c := newCache() ctx := withCacheContext(context.Background(), c) cache, ok := cacheFromContext(ctx) require.True(t, ok) require.Equal(t, c, cache) } func TestCacheFromContext_NotFound(t *testing.T) { cache, ok := cacheFromContext(context.Background()) require.False(t, ok) require.Nil(t, cache) } ================================================ FILE: grype/db/v6/cpe_store.go ================================================ package v6 import ( "fmt" "time" "gorm.io/gorm" "github.com/anchore/go-logger" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) type cpeHandleStore interface { *AffectedCPEHandle | *UnaffectedCPEHandle } type cpeHandleAccessor interface { getCPEHandle() *cpeHandle } type GetCPEOptions struct { PreloadCPE bool PreloadVulnerability bool PreloadBlob bool Vulnerabilities []VulnerabilitySpecifier AllowBroadCPEMatching bool Limit int } type cpeStore struct { db *gorm.DB blobStore *blobStore } func newCPEStore(db *gorm.DB, bs *blobStore) *cpeStore { return &cpeStore{ db: db, blobStore: bs, } } func addCPEs[T cpeHandleStore](s *cpeStore, packages ...T) error { cacheInst, ok := cacheFromContext(s.db.Statement.Context) if !ok { return fmt.Errorf("unable to fetch CPE cache from context") } var final []*Cpe byCacheKey := make(map[string][]*Cpe) for _, p := range packages { ch := any(p).(cpeHandleAccessor).getCPEHandle() if ch.CPE != nil { key := ch.CPE.cacheKey() if existingID, ok := cacheInst.getID(ch.CPE); ok { // seen in a previous transaction... ch.CpeID = existingID } else if _, ok := byCacheKey[key]; !ok { // not seen within this transaction final = append(final, ch.CPE) } byCacheKey[key] = append(byCacheKey[key], ch.CPE) } } if len(final) == 0 { return nil } if err := s.db.Create(final).Error; err != nil { return fmt.Errorf("unable to create CPE records: %w", err) } // update the cache with the new records for _, ref := range final { cacheInst.set(ref) } // update all references with the IDs from the cache for _, refs := range byCacheKey { for _, ref := range refs { id, ok := cacheInst.getID(ref) if ok { ref.setRowID(id) } } } // update the parent objects with the FK ID for _, p := range packages { ch := any(p).(cpeHandleAccessor).getCPEHandle() if ch.CPE != nil { ch.CpeID = ch.CPE.ID } } return nil } func addCPEHandles[T cpeHandleStore](s *cpeStore, packages ...T) error { if err := addCPEs(s, packages...); err != nil { return fmt.Errorf("unable to add CPEs from CPE handles: %w", err) } for _, pkg := range packages { if err := s.blobStore.addBlobable(any(pkg).(blobable)); err != nil { return fmt.Errorf("unable to add CPE handle blob: %w", err) } if err := s.db.Omit("CPE").Create(pkg).Error; err != nil { return fmt.Errorf("unable to add CPE handles: %w", err) } } return nil } func getCPEHandles[T cpeHandleStore]( // nolint:funlen s *cpeStore, cpe *cpe.Attributes, config *GetCPEOptions, tableName string, ) ([]T, error) { if config == nil { config = &GetCPEOptions{} } fields := make(logger.Fields) count := 0 if cpe == nil { fields["cpe"] = "any" } else { fields["cpe"] = cpe.String() } start := time.Now() defer func() { fields["duration"] = time.Since(start) fields["records"] = count log.WithFields(fields).Trace("fetched CPE record") }() query := s.handleCPE(s.db.Table(tableName), cpe, config.AllowBroadCPEMatching, tableName) var err error query, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities, tableName) if err != nil { return nil, err } query = s.handlePreload(query, *config) var models []T var results []T if err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { if config.PreloadBlob { var blobs []blobable for i := range results { blobs = append(blobs, any(results[i]).(blobable)) } if err := s.blobStore.attachBlobValue(blobs...); err != nil { return fmt.Errorf("unable to attach blobs: %w", err) } } if config.PreloadVulnerability { var vulns []blobable for i := range results { ch := any(results[i]).(cpeHandleAccessor).getCPEHandle() if ch.Vulnerability != nil { vulns = append(vulns, ch.Vulnerability) } } if err := s.blobStore.attachBlobValue(vulns...); err != nil { return fmt.Errorf("unable to attach vulnerability blob: %w", err) } } models = append(models, results...) count += len(results) if config.Limit > 0 && len(models) >= config.Limit { return ErrLimitReached } return nil }).Error; err != nil { return models, fmt.Errorf("unable to fetch CPE records: %w", err) } return models, nil } func (s *cpeStore) handleCPE(query *gorm.DB, c *cpe.Attributes, allowBroad bool, tableName string) *gorm.DB { if c == nil { return query } query = query.Joins(fmt.Sprintf("JOIN cpes ON cpes.id = %s.cpe_id", tableName)) return handleCPEOptions(query, c, allowBroad) } func (s *cpeStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier, tableName string) (*gorm.DB, error) { if len(configs) == 0 { return query, nil } query = query.Joins(fmt.Sprintf("JOIN vulnerability_handles ON %s.vulnerability_id = vulnerability_handles.id", tableName)) return handleVulnerabilityOptions(s.db, query, configs...) } func (s *cpeStore) handlePreload(query *gorm.DB, config GetCPEOptions) *gorm.DB { var limitArgs []interface{} if config.Limit > 0 { query = query.Limit(config.Limit) limitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB { return db.Limit(config.Limit) }) } if config.PreloadCPE { query = query.Preload("CPE", limitArgs...) } if config.PreloadVulnerability { query = query.Preload("Vulnerability", limitArgs...).Preload("Vulnerability.Provider", limitArgs...) } return query } ================================================ FILE: grype/db/v6/data.go ================================================ package v6 import ( "strings" "github.com/anchore/syft/syft/pkg" ) // TODO: in a future iteration these should be raised up more explicitly by the vunnel providers func KnownOperatingSystemSpecifierOverrides() []OperatingSystemSpecifierOverride { strRef := func(s string) *string { return &s } return []OperatingSystemSpecifierOverride{ // redhat clones or otherwise shared vulnerability data {Alias: "centos", ReplacementName: strRef("rhel")}, {Alias: "rocky", ReplacementName: strRef("rhel")}, {Alias: "rockylinux", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "rockylinux") {Alias: "alma", ReplacementName: strRef("rhel")}, {Alias: "almalinux", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "almalinux") {Alias: "scientific", ReplacementName: strRef("rhel")}, {Alias: "sl", ReplacementName: strRef("rhel")}, // non-standard, but common (dockerhub uses "sl") {Alias: "gentoo", ReplacementName: strRef("rhel")}, // Alternaitve distros that should match against the debian vulnerability data {Alias: "raspbian", ReplacementName: strRef("debian")}, // to remain backwards compatible, we need to keep old clients from ignoring EUS data. // we do this by diverting any requests for a specific major.minor version of rhel to only // use the major version. But, this only applies to clients before v6.0.3 DB schema version. // Why 6.0.3? This is when OS channel was introduced, which grype-db will leverage, and add additional // rhel rows to the DB, all which have major.minor versions. This means that any old client (which wont // see the new channel column) will assume during OS resolution that there is major.minor vuln data // that should be used (which is incorrect). {Alias: "rhel", VersionPattern: `^\d+\.\d+`, ReplacementMinorVersion: strRef(""), ApplicableClientDBSchemas: "< 6.0.3"}, // we pass in the distro.Type into the search specifier, not a raw release-id {Alias: "redhat", VersionPattern: `^\d+\.\d+`, ReplacementMinorVersion: strRef(""), ReplacementName: strRef("rhel"), ApplicableClientDBSchemas: "< 6.0.3"}, // alpine family {Alias: "alpine", VersionPattern: `.*_alpha.*`, ReplacementLabelVersion: strRef("edge"), Rolling: true}, {Alias: "wolfi", Rolling: true}, {Alias: "chainguard", Rolling: true}, {Alias: "secureos", Rolling: true}, // others {Alias: "archlinux", Rolling: true}, {Alias: "minimos", Rolling: true}, {Alias: "arch", ReplacementName: strRef("archlinux"), Rolling: true}, // os-release ID=arch, but namespace uses archlinux {Alias: "oracle", ReplacementName: strRef("ol")}, // non-standard, but common {Alias: "oraclelinux", ReplacementName: strRef("ol")}, // non-standard, but common (dockerhub uses "oraclelinux") {Alias: "amazon", ReplacementName: strRef("amzn")}, // non-standard, but common {Alias: "amazonlinux", ReplacementName: strRef("amzn")}, // non-standard, but common (dockerhub uses "amazonlinux") {Alias: "echo", Rolling: true}, // TODO: forky is a placeholder for now, but should be updated to sid when the time comes // this needs to be automated, but isn't clear how to do so since you'll see things like this: // // ❯ docker run --rm debian:sid cat /etc/os-release | grep VERSION_CODENAME // VERSION_CODENAME=forky // ❯ docker run --rm debian:testing cat /etc/os-release | grep VERSION_CODENAME // VERSION_CODENAME=forky // // ❯ curl -s http://deb.debian.org/debian/dists/testing/Release | grep '^Codename:' // Codename: forky // ❯ curl -s http://deb.debian.org/debian/dists/sid/Release | grep '^Codename:' // Codename: sid // // depending where the team is during the development cycle you will see different behavior, making automating // this a little challenging. {Alias: "debian", Codename: "forky", Rolling: true, ReplacementLabelVersion: strRef("unstable")}, // is currently sid, which is considered rolling // postmarketOS: map to correct underlying base alpine release version per https://wiki.postmarketos.org/wiki/Releases // NOTE: These are not the values as-is from the corresponding /etc/os-release files, these are the values after grype has parsed // the raw linux.Release objects from syft into grype Distro objects, so for instance the v prefix on the postmarketos VERSION_ID fields // is removed here. // edge is specified in the VERSION_ID field pf the /etc/os-release file for postmarketos, and there is no codename; however, // to be resilient handle both cases where edge may be parsed as the raw version or as the codename {Alias: "postmarketos", Version: "edge", ReplacementName: strRef("alpine"), ReplacementLabelVersion: strRef("edge"), Rolling: true}, {Alias: "postmarketos", Codename: "edge", ReplacementName: strRef("alpine"), ReplacementLabelVersion: strRef("edge"), Rolling: true}, {Alias: "postmarketos", Version: "25.12", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("23")}, {Alias: "postmarketos", Version: "25.06", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("22")}, {Alias: "postmarketos", Version: "24.12", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("21")}, {Alias: "postmarketos", Version: "24.06", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("20")}, {Alias: "postmarketos", Version: "23.12", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("19")}, {Alias: "postmarketos", Version: "23.06", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("18")}, {Alias: "postmarketos", Version: "22.12", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("17")}, {Alias: "postmarketos", Version: "22.06", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("16")}, {Alias: "postmarketos", Version: "21.12", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("15")}, {Alias: "postmarketos", Version: "21.06", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("14")}, {Alias: "postmarketos", Version: "21.03", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("13")}, {Alias: "postmarketos", Version: "20.05", ReplacementName: strRef("alpine"), ReplacementMajorVersion: strRef("3"), ReplacementMinorVersion: strRef("12")}, // If no version is specified, map generally to alpine which has same behaviour as today where it matches against all possible // alpine releases, otherwise we will get no matches. // NOTE: We have to use a hack here with VersionPattern matching empty string because setting Version to "" with no other // primary key properties set breaks matching against the above release mappings {Alias: "postmarketos", VersionPattern: "^$", ReplacementName: strRef("alpine")}, } } func KnownPackageSpecifierOverrides() []PackageSpecifierOverride { // when matching packages, grype will always attempt to do so based off of the package type which means // that any request must be in terms of the package type (relative to syft). ret := []PackageSpecifierOverride{ // map all known language ecosystems to their respective syft package types {Ecosystem: pkg.Dart.String(), ReplacementEcosystem: ptr(string(pkg.DartPubPkg))}, {Ecosystem: pkg.Dotnet.String(), ReplacementEcosystem: ptr(string(pkg.DotnetPkg))}, {Ecosystem: pkg.Elixir.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))}, {Ecosystem: pkg.Erlang.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))}, // Erlang packages use hex.pm, same as Elixir {Ecosystem: string(pkg.ErlangOTPPkg), ReplacementEcosystem: ptr(string(pkg.HexPkg))}, // remap erlang-otp to hex for GHSA matching {Ecosystem: pkg.Go.String(), ReplacementEcosystem: ptr(string(pkg.GoModulePkg))}, {Ecosystem: pkg.Haskell.String(), ReplacementEcosystem: ptr(string(pkg.HackagePkg))}, {Ecosystem: pkg.Java.String(), ReplacementEcosystem: ptr(string(pkg.JavaPkg))}, {Ecosystem: pkg.JavaScript.String(), ReplacementEcosystem: ptr(string(pkg.NpmPkg))}, {Ecosystem: pkg.Lua.String(), ReplacementEcosystem: ptr(string(pkg.LuaRocksPkg))}, {Ecosystem: pkg.OCaml.String(), ReplacementEcosystem: ptr(string(pkg.OpamPkg))}, {Ecosystem: pkg.PHP.String(), ReplacementEcosystem: ptr(string(pkg.PhpComposerPkg))}, {Ecosystem: pkg.Python.String(), ReplacementEcosystem: ptr(string(pkg.PythonPkg))}, {Ecosystem: pkg.R.String(), ReplacementEcosystem: ptr(string(pkg.Rpkg))}, {Ecosystem: pkg.Ruby.String(), ReplacementEcosystem: ptr(string(pkg.GemPkg))}, {Ecosystem: pkg.Rust.String(), ReplacementEcosystem: ptr(string(pkg.RustPkg))}, {Ecosystem: pkg.Swift.String(), ReplacementEcosystem: ptr(string(pkg.SwiftPkg))}, {Ecosystem: pkg.Swipl.String(), ReplacementEcosystem: ptr(string(pkg.SwiplPackPkg))}, // jenkins plugins are a special case since they are always considered to be within the java ecosystem {Ecosystem: string(pkg.JenkinsPluginPkg), ReplacementEcosystem: ptr(string(pkg.JavaPkg))}, // legacy cases {Ecosystem: "pecl", ReplacementEcosystem: ptr(string(pkg.PhpPeclPkg))}, {Ecosystem: "kb", ReplacementEcosystem: ptr(string(pkg.KbPkg))}, {Ecosystem: "dpkg", ReplacementEcosystem: ptr(string(pkg.DebPkg))}, {Ecosystem: "apkg", ReplacementEcosystem: ptr(string(pkg.ApkPkg))}, } // remap package URL types to syft package types for _, t := range pkg.AllPkgs { // these types should never be mapped to // jenkins plugin: java-archive supersedes this // github action workflow: github-action supersedes this switch t { case pkg.JenkinsPluginPkg, pkg.GithubActionWorkflowPkg: continue } purlType := t.PackageURLType() if purlType == "" || purlType == string(t) || strings.HasPrefix(purlType, "generic") { continue } ret = append(ret, PackageSpecifierOverride{ Ecosystem: purlType, ReplacementEcosystem: ptr(string(t)), }) } return ret } func ptr[T any](v T) *T { return &v } ================================================ FILE: grype/db/v6/db.go ================================================ package v6 import ( "context" "fmt" "io" "path/filepath" "gorm.io/gorm" "github.com/anchore/grype/grype/db/internal/gormadapter" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) const ( // We follow SchemaVer semantics (see https://snowplow.io/blog/introducing-schemaver-for-semantic-versioning-of-schemas) // ModelVersion indicates how many breaking schema changes there have been (which will prevent interaction with any historical data) // note: this must ALWAYS be "6" in the context of this package. ModelVersion = 6 // Revision indicates how many changes have been introduced which **may** prevent interaction with some historical data Revision = 1 // Addition indicates how many changes have been introduced that are compatible with all historical data Addition = 4 // v6 model changelog: // 6.0.0: Initial version 🎉 // 6.0.1: Add CISA KEV to VulnerabilityDecorator store // 6.0.2: Add EPSS to VulnerabilityDecorator store // 6.0.3: Add channel column to OperatingSystem model // 6.1.0: Add Fix availability information to AffectedPackageBlob.Range.Fix.Detail. // Existing git commit and timestamp information was removed (as it was unused) // 6.1.1: Add UnaffectedCPE / UnaffectedPackage models and stores (remove "Affected" prefixes from existing blobs) // 6.1.2: Add CWEs // 6.1.3: Add ID field to Reference (for advisory IDs like RHSA-2023:5455) // 6.1.4: Add EOLDate and EOASDate fields to OperatingSystem model ) const ( VulnerabilityDBFileName = "vulnerability.db" // batchSize affects how many records are fetched at a time from the DB. Note: when using preload, row entries // for related records may convey as parameters in a "WHERE x in (...)" which can lead to a large number of // parameters in the query -- if above 999 then this will result in an error for sqlite. For this reason we // try to keep this value well below 999. batchSize = 300 ) var ErrDBCapabilityNotSupported = fmt.Errorf("capability not supported by DB") type ReadWriter interface { Reader Writer } type Reader interface { DBMetadataStoreReader ProviderStoreReader VulnerabilityStoreReader VulnerabilityDecoratorStoreReader OperatingSystemStoreReader AffectedPackageStoreReader UnaffectedPackageStoreReader AffectedCPEStoreReader UnaffectedCPEStoreReader io.Closer attachBlobValue(...blobable) error } type Writer interface { DBMetadataStoreWriter ProviderStoreWriter VulnerabilityStoreWriter VulnerabilityDecoratorStoreWriter OperatingSystemStoreWriter AffectedPackageStoreWriter UnaffectedPackageStoreWriter AffectedCPEStoreWriter UnaffectedCPEStoreWriter io.Closer } type Curator interface { Reader() (Reader, error) Status() vulnerability.ProviderStatus Delete() error Update() (bool, error) Import(dbArchivePath string) error } type Config struct { DBDirPath string Debug bool } func (c Config) DBFilePath() string { return filepath.Join(c.DBDirPath, VulnerabilityDBFileName) } func NewReader(cfg Config) (Reader, error) { return newStore(cfg, false, false) } func NewWriter(cfg Config) (ReadWriter, error) { return newStore(cfg, true, true) } func Hydrater() func(string) error { return func(path string) error { // this will auto-migrate any models, creating and populating indexes as needed // we don't pass any data initialization here because the data is already in the db archive and we do not want // to affect the entries themselves, only indexes and schema. s, err := newStore(Config{DBDirPath: path}, false, true) if s != nil { log.CloseAndLogError(s, path) } return err } } // NewLowLevelDB creates a new empty DB for writing or opens an existing one for reading from the given path. This is // not recommended for typical interactions with the vulnerability DB, use NewReader and NewWriter instead. func NewLowLevelDB(dbFilePath string, empty, writable, debug bool) (*gorm.DB, error) { opts := []gormadapter.Option{ gormadapter.WithDebug(debug), } if empty && !writable { return nil, fmt.Errorf("cannot open an empty database for reading only") } if empty { opts = append(opts, gormadapter.WithTruncate(true, Models(), InitialData()), ) } else if writable { opts = append(opts, gormadapter.WithWritable(true, Models())) } dbObj, err := gormadapter.Open(dbFilePath, opts...) if err != nil { return nil, err } if empty { // speed up writes by persisting key-to-ID lookups when writing to the DB dbObj = dbObj.WithContext(withCacheContext(context.Background(), newCache())) } return dbObj, err } ================================================ FILE: grype/db/v6/db_metadata_store.go ================================================ package v6 import ( "fmt" "time" "gorm.io/gorm" "github.com/anchore/grype/internal/log" ) type DBMetadataStoreWriter interface { SetDBMetadata() error } type DBMetadataStoreReader interface { GetDBMetadata() (*DBMetadata, error) } type dbMetadataStore struct { db *gorm.DB } func newDBMetadataStore(db *gorm.DB) *dbMetadataStore { return &dbMetadataStore{ db: db, } } func (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) { var model DBMetadata result := s.db.First(&model) return &model, result.Error } func (s *dbMetadataStore) SetDBMetadata() error { log.Trace("writing DB metadata") if err := s.db.Where("true").Delete(&DBMetadata{}).Error; err != nil { return fmt.Errorf("failed to delete existing DB metadata record: %w", err) } // note: it is important to round the time to the second to avoid issues with the database update check. // since we are comparing timestamps that are RFC3339 formatted, it's possible that milliseconds will // be rounded up, causing a slight difference in candidate timestamps vs current DB timestamps. ts := time.Now().UTC().Round(time.Second) instance := &DBMetadata{ BuildTimestamp: &ts, Model: ModelVersion, Revision: Revision, Addition: Addition, } if err := s.db.Create(instance).Error; err != nil { return fmt.Errorf("failed to create DB metadata record: %w", err) } return nil } ================================================ FILE: grype/db/v6/db_metadata_store_test.go ================================================ package v6 import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestDbMetadataStore_empty(t *testing.T) { db := setupTestStore(t).db require.NoError(t, db.Where("true").Delete(&DBMetadata{}).Error) // delete all existing records s := newDBMetadataStore(db) // attempt to fetch a non-existent record actualMetadata, err := s.GetDBMetadata() require.ErrorIs(t, err, gorm.ErrRecordNotFound) require.NotNil(t, actualMetadata) } func TestDbMetadataStore_oldDb(t *testing.T) { db := setupTestStore(t).db require.NoError(t, db.Where("true").Model(DBMetadata{}).Update("Model", "5").Error) // old database version s := newDBMetadataStore(db) // attempt to fetch a non-existent record actualMetadata, err := s.GetDBMetadata() require.NoError(t, err) require.NotNil(t, actualMetadata) } func TestDbMetadataStore(t *testing.T) { s := newDBMetadataStore(setupTestStore(t).db) require.NoError(t, s.SetDBMetadata()) // fetch the record actualMetadata, err := s.GetDBMetadata() require.NoError(t, err) require.NotNil(t, actualMetadata) assert.NotZero(t, *actualMetadata.BuildTimestamp) // a timestamp was set name, _ := actualMetadata.BuildTimestamp.Zone() assert.Equal(t, "UTC", name) // the timestamp is in UTC actualMetadata.BuildTimestamp = nil // value not under test assert.Equal(t, DBMetadata{ BuildTimestamp: nil, // expect the correct version info Model: ModelVersion, Revision: Revision, Addition: Addition, }, *actualMetadata) } func setupTestStore(t testing.TB, d ...string) *store { var dir string switch len(d) { case 0: dir = t.TempDir() case 1: dir = d[0] default: t.Fatal("too many arguments") } s, err := newStore(Config{ DBDirPath: dir, }, true, true) require.NoError(t, err) require.NoError(t, s.SetDBMetadata()) return s } func setupReadOnlyTestStore(t testing.TB, dir string) *store { s, err := newStore(Config{ DBDirPath: dir, }, false, false) require.NoError(t, err) return s } ================================================ FILE: grype/db/v6/description.go ================================================ package v6 import ( "errors" "fmt" "os" "path/filepath" "time" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/schemaver" ) var ErrDBDoesNotExist = errors.New("database does not exist") type Description struct { // SchemaVersion is the version of the DB schema SchemaVersion schemaver.SchemaVer `json:"schemaVersion,omitempty"` // Built is the timestamp the database was built Built Time `json:"built"` } type Time struct { time.Time } func (t Time) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("%q", t.String())), nil } func (t *Time) UnmarshalJSON(data []byte) error { str := string(data) if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' { return fmt.Errorf("invalid time format") } str = str[1 : len(str)-1] parsedTime, err := time.Parse(time.RFC3339, str) if err != nil { return err } t.Time = parsedTime.In(time.UTC) return nil } func (t Time) String() string { return t.Time.UTC().Round(time.Second).Format(time.RFC3339) } func DescriptionFromMetadata(m *DBMetadata) *Description { if m == nil { return nil } return &Description{ SchemaVersion: schemaver.New(m.Model, m.Revision, m.Addition), Built: Time{Time: *m.BuildTimestamp}, } } func ReadDescription(dbFilePath string) (*Description, error) { // check if exists if _, err := os.Stat(dbFilePath); err != nil { if errors.Is(err, os.ErrNotExist) { return nil, ErrDBDoesNotExist } return nil, fmt.Errorf("failed to access database file: %w", err) } // access the DB to get the built time and schema version r, err := NewReader(Config{ DBDirPath: filepath.Dir(dbFilePath), }) if err != nil { return nil, fmt.Errorf("failed to read DB description: %w", err) } // we need to ensure readers are closed, or we can see stale reads in new readers! defer log.CloseAndLogError(r, dbFilePath) meta, err := r.GetDBMetadata() if err != nil { return nil, fmt.Errorf("failed to read DB metadata: %w", err) } return &Description{ SchemaVersion: schemaver.New(meta.Model, meta.Revision, meta.Addition), Built: Time{Time: *meta.BuildTimestamp}, }, nil } func (m Description) String() string { return fmt.Sprintf("DB(version=%s built=%s)", m.SchemaVersion, m.Built) } ================================================ FILE: grype/db/v6/description_test.go ================================================ package v6 import ( "encoding/json" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/internal/schemaver" ) func TestReadDescription(t *testing.T) { tempDir := t.TempDir() s, err := NewWriter(Config{DBDirPath: tempDir}) require.NoError(t, err) require.NoError(t, s.SetDBMetadata()) expected, err := s.GetDBMetadata() require.NoError(t, err) require.NoError(t, s.Close()) dbFilePath := filepath.Join(tempDir, VulnerabilityDBFileName) description, err := ReadDescription(dbFilePath) require.NoError(t, err) require.NotNil(t, description) assert.Equal(t, Description{ SchemaVersion: schemaver.New(expected.Model, expected.Revision, expected.Addition), Built: Time{*expected.BuildTimestamp}, }, *description) } func TestTime_JSONMarshalling(t *testing.T) { tests := []struct { name string time Time expected string }{ { name: "go case", time: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, expected: `"2023-09-26T12:00:00Z"`, }, { name: "convert to utc", time: Time{time.Date(2023, 9, 26, 13, 0, 0, 0, time.FixedZone("UTC+1", 3600))}, expected: `"2023-09-26T12:00:00Z"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { jsonData, err := json.Marshal(tt.time) require.NoError(t, err) require.Equal(t, tt.expected, string(jsonData)) }) } } func TestTime_JSONUnmarshalling(t *testing.T) { tests := []struct { name string jsonData string expectedTime Time expectError require.ErrorAssertionFunc }{ { name: "use zulu offset", jsonData: `"2023-09-26T12:00:00Z"`, expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, }, { name: "use tz offset in another timezone", jsonData: `"2023-09-26T14:00:00+02:00"`, expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, }, { name: "use tz offset that is utc", jsonData: `"2023-09-26T12:00:00+00:00"`, expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, }, { name: "invalid format", jsonData: `"invalid-time-format"`, expectError: require.Error, }, { name: "invalid json", jsonData: `invalid`, expectError: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectError == nil { tt.expectError = require.NoError } var parsedTime Time err := json.Unmarshal([]byte(tt.jsonData), &parsedTime) tt.expectError(t, err) if err == nil { assert.Equal(t, tt.expectedTime.Time, parsedTime.Time) } }) } } ================================================ FILE: grype/db/v6/distribution/client.go ================================================ package distribution import ( "crypto/tls" "crypto/x509" "fmt" "net/http" "net/url" "os" "path" "strings" "time" "github.com/hashicorp/go-cleanhttp" "github.com/spf13/afero" "github.com/wagoodman/go-progress" "github.com/anchore/clio" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" ) type Config struct { ID clio.Identification // check/fetch parameters LatestURL string CACert string // validations RequireUpdateCheck bool // timeouts CheckTimeout time.Duration UpdateTimeout time.Duration } type Client interface { Latest() (*LatestDocument, error) IsUpdateAvailable(current *v6.Description) (*Archive, error) ResolveArchiveURL(archive Archive) (string, error) Download(url, dest string, downloadProgress *progress.Manual) (string, error) } type client struct { fs afero.Fs dbDownloader file.Getter listingDownloader file.Getter config Config } func DefaultConfig() Config { return Config{ LatestURL: "https://grype.anchore.io/databases", RequireUpdateCheck: false, CheckTimeout: 30 * time.Second, UpdateTimeout: 300 * time.Second, } } func NewClient(cfg Config) (Client, error) { fs := afero.NewOsFs() latestClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.CheckTimeout), withUserAgent(cfg.ID)) if err != nil { return client{}, err } dbClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.UpdateTimeout), withUserAgent(cfg.ID)) if err != nil { return client{}, err } return client{ fs: fs, listingDownloader: file.NewGetter(cfg.ID, latestClient), dbDownloader: file.NewGetter(cfg.ID, dbClient), config: cfg, }, nil } // IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest db information // available for this schema. func (c client) IsUpdateAvailable(current *v6.Description) (*Archive, error) { log.Debugf("checking for available database updates") latestDoc, err := c.Latest() if err != nil { if c.config.RequireUpdateCheck { return nil, fmt.Errorf("check for vulnerability database update failed: %+v", err) } log.Warnf("unable to check for vulnerability database update") log.Debugf("check for vulnerability update failed: %+v", err) } archive, message := c.isUpdateAvailable(current, latestDoc) if message != "" { log.Warn(message) bus.Notify(message) } return archive, err } func (c client) isUpdateAvailable(current *v6.Description, candidate *LatestDocument) (*Archive, string) { if candidate == nil { return nil, "" } var message string switch candidate.Status { case StatusDeprecated: message = "this version of grype will soon stop receiving vulnerability database updates, please update grype" case StatusEndOfLife: message = "this version of grype is no longer receiving vulnerability database updates, please update grype" } // compare created data to current db date if isSupersededBy(current, candidate.Description) { log.Debugf("database update available: %s", candidate.Description) return &candidate.Archive, message } log.Debugf("no database update available") return nil, message } func (c client) ResolveArchiveURL(archive Archive) (string, error) { // download the db to the temp dir u, err := url.Parse(c.latestURL()) if err != nil { return "", fmt.Errorf("unable to parse db URL %q: %w", c.latestURL(), err) } u.Path = path.Join(path.Dir(u.Path), path.Clean(archive.Path)) // from go-getter, adding a checksum as a query string will validate the payload after download // note: the checksum query parameter is not sent to the server query := u.Query() if archive.Checksum != "" { query.Add("checksum", archive.Checksum) } u.RawQuery = query.Encode() return u.String(), nil } func (c client) Download(archiveURL, dest string, downloadProgress *progress.Manual) (string, error) { defer downloadProgress.SetCompleted() if err := os.MkdirAll(dest, 0700); err != nil { return "", fmt.Errorf("unable to create db download root dir: %w", err) } // note: as much as I'd like to use the afero FS abstraction here, the go-getter library does not support it tempDir, err := os.MkdirTemp(dest, "grype-db-download") if err != nil { return "", fmt.Errorf("unable to create db client temp dir: %w", err) } // go-getter will automatically extract all files within the archive to the temp dir err = c.dbDownloader.GetToDir(tempDir, archiveURL, downloadProgress) if err != nil { removeAllOrLog(afero.NewOsFs(), tempDir) return "", fmt.Errorf("unable to download db: %w", err) } return tempDir, nil } // Latest loads a LatestDocument from the configured URL. func (c client) Latest() (*LatestDocument, error) { tempFile, err := afero.TempFile(c.fs, "", "grype-db-listing") if err != nil { return nil, fmt.Errorf("unable to create listing temp file: %w", err) } defer func() { log.CloseAndLogError(tempFile, tempFile.Name()) err := c.fs.RemoveAll(tempFile.Name()) if err != nil { log.WithFields("error", err, "file", tempFile.Name()).Errorf("failed to remove file") } }() err = c.listingDownloader.GetFile(tempFile.Name(), c.latestURL()) if err != nil { return nil, fmt.Errorf("unable to download listing: %w", err) } return NewLatestFromFile(c.fs, tempFile.Name()) } func (c client) latestURL() string { u := c.config.LatestURL // allow path to be specified directly to a json file, or the path without version information if !strings.HasSuffix(u, ".json") { u = strings.TrimRight(u, "/") u = fmt.Sprintf("%s/v%d/%s", u, v6.ModelVersion, LatestFileName) } return u } func withClientTimeout(timeout time.Duration) func(*http.Client) { return func(c *http.Client) { c.Timeout = timeout } } func withUserAgent(id clio.Identification) func(*http.Client) { return func(c *http.Client) { *(c) = *newHTTPClientWithDefaultUserAgent(c.Transport, fmt.Sprintf("%s %s", id.Name, id.Version)) } } func defaultHTTPClient(fs afero.Fs, caCertPath string, postProcessor ...func(*http.Client)) (*http.Client, error) { httpClient := cleanhttp.DefaultClient() httpClient.Timeout = 30 * time.Second if caCertPath != "" { rootCAs := x509.NewCertPool() pemBytes, err := afero.ReadFile(fs, caCertPath) if err != nil { return nil, fmt.Errorf("unable to configure root CAs for curator: %w", err) } rootCAs.AppendCertsFromPEM(pemBytes) httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: rootCAs, } } for _, pp := range postProcessor { pp(httpClient) } return httpClient, nil } func removeAllOrLog(fs afero.Fs, dir string) { if err := fs.RemoveAll(dir); err != nil { log.WithFields("error", err).Warnf("failed to remove path %q", dir) } } func isSupersededBy(current *v6.Description, candidate v6.Description) bool { if current == nil { log.Debug("cannot find existing metadata, using update...") // any valid update beats no database, use it! return true } if !current.SchemaVersion.Valid() { log.Error("existing database has no schema version, doing nothing...") return false } if !candidate.SchemaVersion.Valid() { log.Error("update has no schema version, doing nothing...") return false } if candidate.SchemaVersion.Model != current.SchemaVersion.Model { log.WithFields("want", current.SchemaVersion.Model, "received", candidate.SchemaVersion.Model).Warn("update is for a different DB schema, skipping...") return false } if candidate.Built.After(current.Built.Time) { d := candidate.Built.Sub(current.Built.Time).String() log.WithFields("existing", current.Built.String(), "candidate", candidate.Built.String(), "delta", d).Debug("existing database is older than candidate update, using update...") // the listing is newer than the existing db, use it! return true } log.Debugf("existing database is already up to date") return false } func newHTTPClientWithDefaultUserAgent(baseTransport http.RoundTripper, userAgent string) *http.Client { return &http.Client{ Transport: roundTripperWithUserAgent{ transport: baseTransport, userAgent: userAgent, }, } } type roundTripperWithUserAgent struct { transport http.RoundTripper userAgent string } func (r roundTripperWithUserAgent) RoundTrip(req *http.Request) (*http.Response, error) { clonedReq := req.Clone(req.Context()) if clonedReq.Header.Get("User-Agent") == "" { clonedReq.Header.Set("User-Agent", r.userAgent) } return r.transport.RoundTrip(clonedReq) } ================================================ FILE: grype/db/v6/distribution/client_test.go ================================================ package distribution import ( "encoding/json" "errors" "path/filepath" "testing" "time" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/wagoodman/go-progress" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/schemaver" ) type mockGetter struct { mock.Mock } func (m *mockGetter) GetFile(dst, src string, manuals ...*progress.Manual) error { args := m.Called(dst, src, manuals) return args.Error(0) } func (m *mockGetter) GetToDir(dst, src string, manuals ...*progress.Manual) error { args := m.Called(dst, src, manuals) return args.Error(0) } func TestClient_Latest(t *testing.T) { tests := []struct { name string latestResponse []byte getFileErr error expectedDoc *LatestDocument expectedErr require.ErrorAssertionFunc }{ { name: "go case", latestResponse: func() []byte { doc := LatestDocument{ Status: "active", Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive", Checksum: "checksum123", }, } data, err := json.Marshal(doc) require.NoError(t, err) return data }(), expectedDoc: &LatestDocument{ Status: "active", Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive", Checksum: "checksum123", }, }, }, { name: "download error", getFileErr: errors.New("failed to download file"), expectedDoc: nil, expectedErr: func(t require.TestingT, err error, _ ...interface{}) { require.Error(t, err) require.Contains(t, err.Error(), "unable to download listing") }, }, { name: "malformed JSON response", latestResponse: []byte("malformed json"), expectedDoc: nil, expectedErr: func(t require.TestingT, err error, _ ...interface{}) { require.Error(t, err) require.Contains(t, err.Error(), "invalid character 'm' looking for beginning of value") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectedErr == nil { tt.expectedErr = require.NoError } mockFs := afero.NewMemMapFs() mg := new(mockGetter) mg.On("GetFile", mock.Anything, "http://localhost:8080/latest.json", mock.Anything).Run(func(args mock.Arguments) { if tt.getFileErr != nil { return } dst := args.String(0) err := afero.WriteFile(mockFs, dst, tt.latestResponse, 0644) require.NoError(t, err) }).Return(tt.getFileErr) c, err := NewClient(Config{ LatestURL: "http://localhost:8080/latest.json", }) require.NoError(t, err) cl := c.(client) cl.fs = mockFs cl.listingDownloader = mg doc, err := cl.Latest() tt.expectedErr(t, err) if err != nil { return } require.Equal(t, tt.expectedDoc, doc) mg.AssertExpectations(t) }) } } func TestClient_Download(t *testing.T) { destDir := t.TempDir() setup := func() (Client, *mockGetter) { mg := new(mockGetter) c, err := NewClient(Config{ LatestURL: "http://localhost:8080/latest.json", }) require.NoError(t, err) cl := c.(client) cl.dbDownloader = mg return cl, mg } t.Run("successful download", func(t *testing.T) { c, mg := setup() url := "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123" mg.On("GetToDir", mock.Anything, url, mock.Anything).Return(nil) tempDir, err := c.Download(url, destDir, &progress.Manual{}) require.NoError(t, err) require.True(t, len(tempDir) > 0) mg.AssertExpectations(t) }) t.Run("download error", func(t *testing.T) { c, mg := setup() url := "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123" mg.On("GetToDir", mock.Anything, url, mock.Anything).Return(errors.New("download failed")) tempDir, err := c.Download(url, destDir, &progress.Manual{}) require.Error(t, err) require.Empty(t, tempDir) require.Contains(t, err.Error(), "unable to download db") mg.AssertExpectations(t) }) t.Run("nested into dir that does not exist", func(t *testing.T) { c, mg := setup() url := "http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123" mg.On("GetToDir", mock.Anything, url, mock.Anything).Return(nil) nestedPath := filepath.Join(destDir, "nested") tempDir, err := c.Download(url, nestedPath, &progress.Manual{}) require.NoError(t, err) require.True(t, len(tempDir) > 0) mg.AssertExpectations(t) }) } func TestClient_IsUpdateAvailable(t *testing.T) { current := &db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, } tests := []struct { name string candidate *LatestDocument archive *Archive message string }{ { name: "update available", candidate: &LatestDocument{ Status: StatusActive, Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, }, archive: &Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, }, { name: "no update available", candidate: &LatestDocument{ Status: "active", Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, }, archive: nil, }, { name: "no candidate available", candidate: nil, archive: nil, }, { name: "candidate deprecated", candidate: &LatestDocument{ Status: StatusDeprecated, Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, }, archive: &Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, message: "this version of grype will soon stop receiving vulnerability database updates, please update grype", }, { name: "candidate end of life", candidate: &LatestDocument{ Status: StatusEndOfLife, Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, }, archive: &Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", }, message: "this version of grype is no longer receiving vulnerability database updates, please update grype", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, err := NewClient(Config{}) require.NoError(t, err) cl := c.(client) archive, message := cl.isUpdateAvailable(current, tt.candidate) assert.Equal(t, tt.message, message) assert.Equal(t, tt.archive, archive) }) } } func TestDatabaseDescription_IsSupersededBy(t *testing.T) { t1 := time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC) t2 := time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC) currentMetadata := db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: t1}, } newerMetadata := db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: t2}, } olderMetadata := db.Description{ SchemaVersion: schemaver.New(1, 0, 0), Built: db.Time{Time: t1}, } differentModelMetadata := db.Description{ SchemaVersion: schemaver.New(2, 0, 0), Built: db.Time{Time: t2}, } tests := []struct { name string current *db.Description other db.Description expected bool }{ { name: "no current metadata", current: nil, other: newerMetadata, expected: true, }, { name: "newer build", current: ¤tMetadata, other: newerMetadata, expected: true, }, { name: "older build", current: ¤tMetadata, other: olderMetadata, expected: false, }, { name: "different schema version", current: ¤tMetadata, other: differentModelMetadata, expected: false, }, { name: "current metadata has no schema version", current: &db.Description{Built: db.Time{Time: t1}}, other: newerMetadata, expected: false, }, { name: "update has no schema version", current: ¤tMetadata, other: db.Description{Built: db.Time{Time: t2}}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isSupersededBy(tt.current, tt.other) require.Equal(t, tt.expected, result) }) } } func Test_latestURL(t *testing.T) { tests := []struct { url string expected string }{ { url: "https://grype.anchore.io/databases", expected: "https://grype.anchore.io/databases/v6/latest.json", }, { url: "https://grype.anchore.io/databases/", expected: "https://grype.anchore.io/databases/v6/latest.json", }, { url: "https://grype.anchore.io/databases/v6/latest.json", expected: "https://grype.anchore.io/databases/v6/latest.json", }, { url: "http://grype.anchore.io/databases/", expected: "http://grype.anchore.io/databases/v6/latest.json", }, { url: "https://example.com/file.json", expected: "https://example.com/file.json", }, } for _, test := range tests { t.Run(test.url, func(t *testing.T) { c := client{ config: Config{ LatestURL: test.url, }, } got := c.latestURL() require.Equal(t, test.expected, got) }) } } ================================================ FILE: grype/db/v6/distribution/latest.go ================================================ package distribution import ( "crypto/sha256" "encoding/json" "fmt" "io" "path/filepath" "sort" "time" "github.com/spf13/afero" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/schemaver" ) const LatestFileName = "latest.json" type LatestDocument struct { // Status indicates if the database is actively being maintained and distributed Status Status `json:"status"` // Archive is the most recent database that has been built and distributed, additionally annotated with provider-level information Archive `json:",inline"` } type Archive struct { // Description contains details about the database contained within the distribution archive db.Description `json:",inline"` // Path is the path to a DB archive relative to the listing file hosted location. // Note: this is NOT the absolute URL to download the database. Path string `json:"path"` // Checksum is the self describing digest of the database archive referenced in path Checksum string `json:"checksum"` } func NewLatestDocument(entries ...Archive) *LatestDocument { var validEntries []Archive for _, entry := range entries { if entry.SchemaVersion.Model == db.ModelVersion { validEntries = append(validEntries, entry) } } if len(validEntries) == 0 { return nil } // sort from most recent to the least recent sort.SliceStable(validEntries, func(i, j int) bool { return validEntries[i].Built.After(entries[j].Built.Time) }) return &LatestDocument{ Archive: validEntries[0], Status: LifecycleStatus, } } func NewLatestFromReader(reader io.Reader) (*LatestDocument, error) { var l LatestDocument if err := json.NewDecoder(reader).Decode(&l); err != nil { return nil, fmt.Errorf("unable to parse DB latest.json: %w", err) } if l == (LatestDocument{}) { return nil, nil } return &l, nil } func NewLatestFromFile(fs afero.Fs, path string) (*LatestDocument, error) { fh, err := fs.Open(path) if err != nil { return nil, fmt.Errorf("failed to read listing file: %w", err) } defer fh.Close() return NewLatestFromReader(fh) } func NewArchive(path string, t time.Time, model, revision, addition int) (*Archive, error) { checksum, err := calculateArchiveDigest(path) if err != nil { return nil, fmt.Errorf("failed to calculate archive checksum: %w", err) } return &Archive{ Description: db.Description{ SchemaVersion: schemaver.New(model, revision, addition), Built: db.Time{Time: t}, }, // this is not the path on disk, this is the path relative to the latest.json file when hosted Path: filepath.Base(path), Checksum: checksum, }, nil } func (l LatestDocument) Write(writer io.Writer) error { if l.SchemaVersion.Model == 0 { return fmt.Errorf("missing schema version") } if l.Status == "" { l.Status = LifecycleStatus } if l.Path == "" { return fmt.Errorf("missing archive path") } if l.Checksum == "" { return fmt.Errorf("missing archive checksum") } if l.Built.IsZero() { return fmt.Errorf("missing built time") } contents, err := json.MarshalIndent(&l, "", " ") if err != nil { return fmt.Errorf("failed to encode listing file: %w", err) } _, err = writer.Write(contents) return err } func calculateArchiveDigest(dbFilePath string) (string, error) { digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, sha256.New()) if err != nil { return "", fmt.Errorf("failed to calculate checksum for DB archive file: %w", err) } return fmt.Sprintf("sha256:%s", digest), nil } ================================================ FILE: grype/db/v6/distribution/latest_test.go ================================================ package distribution import ( "bytes" "encoding/json" "os" "path/filepath" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/schemaver" ) func TestNewLatestDocument(t *testing.T) { t.Run("valid entries", func(t *testing.T) { archive1 := Archive{ Description: db.Description{ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Built: db.Time{Time: time.Now()}, }, } archive2 := Archive{ Description: db.Description{ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Built: db.Time{Time: time.Now().Add(-1 * time.Hour)}, }, } latestDoc := NewLatestDocument(archive1, archive2) require.NotNil(t, latestDoc) require.Equal(t, latestDoc.Archive, archive1) // most recent archive require.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion) }) t.Run("filter entries", func(t *testing.T) { archive1 := Archive{ Description: db.Description{ SchemaVersion: schemaver.New(5, db.Revision, db.Addition), // old! Built: db.Time{Time: time.Now()}, }, } archive2 := Archive{ Description: db.Description{ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Built: db.Time{Time: time.Now().Add(-1 * time.Hour)}, }, } latestDoc := NewLatestDocument(archive1, archive2) require.NotNil(t, latestDoc) require.Equal(t, latestDoc.Archive, archive2) // most recent archive with valid version require.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion) }) t.Run("no entries", func(t *testing.T) { latestDoc := NewLatestDocument() require.Nil(t, latestDoc) }) } func TestNewLatestFromReader(t *testing.T) { t.Run("valid JSON", func(t *testing.T) { latestDoc := LatestDocument{ Archive: Archive{ Description: db.Description{ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Built: db.Time{Time: time.Now().Truncate(time.Second).UTC()}, }, }, Status: "active", } var buf bytes.Buffer require.NoError(t, json.NewEncoder(&buf).Encode(latestDoc)) result, err := NewLatestFromReader(&buf) require.NoError(t, err) require.Equal(t, latestDoc.SchemaVersion, result.SchemaVersion) require.Equal(t, latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time) }) t.Run("empty", func(t *testing.T) { emptyJSON := []byte("{}") val, err := NewLatestFromReader(bytes.NewReader(emptyJSON)) require.NoError(t, err) assert.Nil(t, val) }) t.Run("invalid JSON", func(t *testing.T) { invalidJSON := []byte("invalid json") val, err := NewLatestFromReader(bytes.NewReader(invalidJSON)) require.Error(t, err) require.Contains(t, err.Error(), "unable to parse DB latest.json") assert.Nil(t, val) }) } func TestLatestDocument_Write(t *testing.T) { errContains := func(text string) require.ErrorAssertionFunc { return func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.ErrorContains(t, err, text, msgAndArgs...) } } now := db.Time{Time: time.Now().Truncate(time.Second).UTC()} tests := []struct { name string latestDoc LatestDocument expectedError require.ErrorAssertionFunc }{ { name: "valid document", latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ Built: now, SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", Checksum: "sha256:validchecksum", }, // note: status not supplied, should assume to be active }, expectedError: require.NoError, }, { name: "explicit status", latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ Built: now, SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", Checksum: "xxh64:validchecksum", }, Status: StatusDeprecated, }, expectedError: require.NoError, }, { name: "missing schema version", latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ Built: now, }, Path: "valid/path/to/archive", Checksum: "xxh64:validchecksum", }, Status: "active", }, expectedError: errContains("missing schema version"), }, { name: "missing archive path", latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ Built: now, SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "", // this! Checksum: "xxh64:validchecksum", }, Status: "active", }, expectedError: errContains("missing archive path"), }, { name: "missing archive checksum", latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ Built: now, SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", Checksum: "", // this! }, Status: "active", }, expectedError: errContains("missing archive checksum"), }, { name: "missing built time", latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ Built: db.Time{}, // this! SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", Checksum: "xxh64:validchecksum", }, Status: "active", }, expectedError: errContains("missing built time"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectedError == nil { tt.expectedError = require.NoError } var buf bytes.Buffer err := tt.latestDoc.Write(&buf) tt.expectedError(t, err) if err != nil { return } var result LatestDocument assert.NoError(t, json.Unmarshal(buf.Bytes(), &result)) assert.Equal(t, tt.latestDoc.SchemaVersion, result.SchemaVersion, "schema version mismatch") assert.Equal(t, tt.latestDoc.Archive.Checksum, result.Archive.Checksum, "archive checksum mismatch") assert.Equal(t, tt.latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time, "built time mismatch") assert.Equal(t, tt.latestDoc.Archive.Path, result.Archive.Path, "path mismatch") if tt.latestDoc.Status == "" { assert.Equal(t, StatusActive, result.Status, "status mismatch") } else { assert.Equal(t, tt.latestDoc.Status, result.Status, "status mismatch") } }) } } func TestNewArchive(t *testing.T) { tests := []struct { name string contents string time time.Time model int revision int addition int expectErr require.ErrorAssertionFunc expected *Archive }{ { name: "valid input", contents: "test archive content", time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC), model: 1, revision: 0, addition: 5, expectErr: require.NoError, expected: &Archive{ Description: db.Description{ SchemaVersion: schemaver.New(1, 0, 5), Built: db.Time{Time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC)}, }, Path: "archive.tar.gz", Checksum: "sha256:2a11c11d2c3803697c458a1f5f03c2b73235c101f93c88193cc8810003c40d87", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := t.TempDir() tempFile, err := os.Create(filepath.Join(d, tt.expected.Path)) require.NoError(t, err) _, err = tempFile.WriteString(tt.contents) require.NoError(t, err) archive, err := NewArchive(tempFile.Name(), tt.time, tt.model, tt.revision, tt.addition) tt.expectErr(t, err) if err != nil { return } if diff := cmp.Diff(tt.expected, archive); diff != "" { t.Errorf("unexpected archive (-want +got):\n%s", diff) } }) } } ================================================ FILE: grype/db/v6/distribution/status.go ================================================ package distribution type Status string const LifecycleStatus = StatusActive const ( // StatusActive indicates the database is actively being maintained and distributed StatusActive Status = "active" // StatusDeprecated indicates the database is still being distributed but is approaching end of life. Upgrade grype to avoid future disruptions. StatusDeprecated Status = "deprecated" // StatusEndOfLife indicates the database is no longer being distributed. Users must build their own databases or upgrade grype. StatusEndOfLife Status = "eol" ) ================================================ FILE: grype/db/v6/enumerations.go ================================================ package v6 import "strings" // VulnerabilityStatus is meant to convey the current point in the lifecycle for a vulnerability record. // This is roughly based on CVE status, NVD status, and vendor-specific status values (see https://nvd.nist.gov/vuln/vulnerability-status) type VulnerabilityStatus string const ( UnknownVulnerabilityStatus VulnerabilityStatus = "" // VulnerabilityActive means that the information from the vulnerability record is actionable VulnerabilityActive VulnerabilityStatus = "active" // empty also means active // VulnerabilityAnalyzing means that the vulnerability record is being reviewed, it may or may not be actionable VulnerabilityAnalyzing VulnerabilityStatus = "analyzing" // VulnerabilityRejected means that data from the vulnerability record should not be acted upon VulnerabilityRejected VulnerabilityStatus = "rejected" // VulnerabilityDisputed means that the vulnerability record is in contention, it may or may not be actionable VulnerabilityDisputed VulnerabilityStatus = "disputed" ) // SeverityScheme represents how to interpret the string value for a vulnerability severity type SeverityScheme string const ( UnknownSeverityScheme SeverityScheme = "" // SeveritySchemeCVSS is the Common Vulnerability Scoring System severity scheme SeveritySchemeCVSS SeverityScheme = "CVSS" // SeveritySchemeHML is a string severity scheme (High, Medium, Low) SeveritySchemeHML SeverityScheme = "HML" // SeveritySchemeCHML is a string severity scheme (Critical, High, Medium, Low) SeveritySchemeCHML SeverityScheme = "CHML" // SeveritySchemeCHMLN is a string severity scheme (Critical, High, Medium, Low, Negligible) SeveritySchemeCHMLN SeverityScheme = "CHMLN" ) // FixStatus conveys if the package is affected (or not) and the current availability (or not) of a fix type FixStatus string const ( UnknownFixStatus FixStatus = "" // FixedStatus affirms the package is affected and a fix is available FixedStatus FixStatus = "fixed" // NotFixedStatus affirms the package is affected and a fix is not available NotFixedStatus FixStatus = "not-fixed" // WontFixStatus affirms the package is affected and a fix will not be provided WontFixStatus FixStatus = "wont-fix" // NotAffectedFixStatus affirms the package is not affected by the vulnerability NotAffectedFixStatus FixStatus = "not-affected" ) const ( // AdvisoryReferenceTag is a tag that can be used to identify vulnerability advisory URL references AdvisoryReferenceTag = "advisory" ) func ParseVulnerabilityStatus(s string) VulnerabilityStatus { switch strings.TrimSpace(strings.ToLower(s)) { case string(VulnerabilityActive), "": return VulnerabilityActive case string(VulnerabilityAnalyzing): return VulnerabilityAnalyzing case string(VulnerabilityRejected): return VulnerabilityRejected case string(VulnerabilityDisputed): return VulnerabilityDisputed default: return UnknownVulnerabilityStatus } } func ParseSeverityScheme(s string) SeverityScheme { switch replaceAny(strings.TrimSpace(strings.ToLower(s)), "", "-", "_", " ") { case strings.ToLower(string(SeveritySchemeCVSS)): return SeveritySchemeCVSS case strings.ToLower(string(SeveritySchemeHML)): return SeveritySchemeHML case strings.ToLower(string(SeveritySchemeCHML)): return SeveritySchemeCHML case strings.ToLower(string(SeveritySchemeCHMLN)): return SeveritySchemeCHMLN default: return UnknownSeverityScheme } } func ParseFixStatus(s string) FixStatus { switch replaceAny(strings.TrimSpace(strings.ToLower(s)), "-", " ", "_") { case string(FixedStatus): return FixedStatus case string(NotFixedStatus): return NotFixedStatus case string(WontFixStatus): return WontFixStatus case string(NotAffectedFixStatus): return NotAffectedFixStatus default: return UnknownFixStatus } } func NormalizeReferenceTags(tags []string) []string { var normalized []string for _, tag := range tags { normalized = append(normalized, replaceAny(strings.ToLower(strings.TrimSpace(tag)), "-", " ", "_")) } return normalized } func replaceAny(input string, newStr string, searchFor ...string) string { for _, s := range searchFor { input = strings.ReplaceAll(input, s, newStr) } return input } ================================================ FILE: grype/db/v6/enumerations_test.go ================================================ package v6 import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseVulnerabilityStatus(t *testing.T) { tests := []struct { name string input string expected VulnerabilityStatus }{ {"Active status", "active", VulnerabilityActive}, {"Analyzing status with whitespace", " analyzing ", VulnerabilityAnalyzing}, {"Rejected status in uppercase", "REJECTED", VulnerabilityRejected}, {"Disputed status", "disputed", VulnerabilityDisputed}, {"Unknown status", "unknown", UnknownVulnerabilityStatus}, {"Empty string as active status", "", VulnerabilityActive}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, ParseVulnerabilityStatus(tt.input)) }) } } func TestParseSeverityScheme(t *testing.T) { tests := []struct { name string input string expected SeverityScheme }{ {"CVSS scheme", "Cvss", SeveritySchemeCVSS}, {"HML scheme", "H-M-l", SeveritySchemeHML}, {"CHML scheme", "ChmL", SeveritySchemeCHML}, {"CHMLN scheme", "CHmLN", SeveritySchemeCHMLN}, {"Unknown scheme", "unknown", UnknownSeverityScheme}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, ParseSeverityScheme(tt.input)) }) } } func TestParseFixStatus(t *testing.T) { tests := []struct { name string input string expected FixStatus }{ {"Fixed status", "fixed", FixedStatus}, {"Not fixed status with hyphen", "not-fixed", NotFixedStatus}, {"Wont fix status in uppercase with underscore", "WONT_FIX", WontFixStatus}, {"Not affected status with whitespace", " not affected ", NotAffectedFixStatus}, {"Unknown status", "unknown", UnknownFixStatus}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, ParseFixStatus(tt.input)) }) } } func TestReplaceAny(t *testing.T) { tests := []struct { name string input string newStr string searchFor []string expected string }{ {"go case", "really not_fixed-i'promise", "-", []string{"'", " ", "_"}, "really-not-fixed-i-promise"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, replaceAny(tt.input, tt.newStr, tt.searchFor...)) }) } } ================================================ FILE: grype/db/v6/fillers.go ================================================ package v6 import ( "errors" ) // fillAffectedPackageHandles lazy loads all properties on the list of AffectedPackageHandles func fillAffectedPackageHandles(reader Reader, handles []*AffectedPackageHandle) error { return errors.Join( reader.attachBlobValue(toBlobables(handles)...), fillRefs(reader, handles, affectedPackageHandleOperatingSystemRef, operatingSystemID), fillRefs(reader, handles, affectedPackageHandlePackageRef, packageID), fillVulnerabilityHandles(reader, handles, affectedPackageHandleVulnerabilityHandleRef), ) } func affectedPackageHandleOperatingSystemRef(t *AffectedPackageHandle) idRef[OperatingSystem] { return idRef[OperatingSystem]{ id: t.OperatingSystemID, ref: &t.OperatingSystem, } } func affectedPackageHandlePackageRef(t *AffectedPackageHandle) idRef[Package] { return idRef[Package]{ id: &t.PackageID, ref: &t.Package, } } func affectedPackageHandleVulnerabilityHandleRef(t *AffectedPackageHandle) idRef[VulnerabilityHandle] { return idRef[VulnerabilityHandle]{ id: &t.VulnerabilityID, ref: &t.Vulnerability, } } // fillAffectedCPEHandles lazy loads all properties on the list of AffectedCPEHandles func fillAffectedCPEHandles(reader Reader, handles []*AffectedCPEHandle) error { return errors.Join( reader.attachBlobValue(toBlobables(handles)...), fillRefs(reader, handles, affectedCPEHandleCpeRef, cpeHandleID), fillVulnerabilityHandles(reader, handles, affectedCPEHandleVulnerabilityHandleRef), ) } func affectedCPEHandleCpeRef(t *AffectedCPEHandle) idRef[Cpe] { return idRef[Cpe]{ id: &t.CpeID, ref: &t.CPE, } } func affectedCPEHandleVulnerabilityHandleRef(t *AffectedCPEHandle) idRef[VulnerabilityHandle] { return idRef[VulnerabilityHandle]{ id: &t.VulnerabilityID, ref: &t.Vulnerability, } } // fillUnaffectedPackageHandles lazy loads all properties on the list of UnaffectedPackageHandles func fillUnaffectedPackageHandles(reader Reader, handles []*UnaffectedPackageHandle) error { return errors.Join( reader.attachBlobValue(toBlobables(handles)...), fillRefs(reader, handles, unaffectedPackageHandleOperatingSystemRef, operatingSystemID), fillRefs(reader, handles, unaffectedPackageHandlePackageRef, packageID), fillVulnerabilityHandles(reader, handles, unaffectedPackageHandleVulnerabilityHandleRef), ) } func unaffectedPackageHandleOperatingSystemRef(t *UnaffectedPackageHandle) idRef[OperatingSystem] { return idRef[OperatingSystem]{ id: t.OperatingSystemID, ref: &t.OperatingSystem, } } func unaffectedPackageHandlePackageRef(t *UnaffectedPackageHandle) idRef[Package] { return idRef[Package]{ id: &t.PackageID, ref: &t.Package, } } func unaffectedPackageHandleVulnerabilityHandleRef(t *UnaffectedPackageHandle) idRef[VulnerabilityHandle] { return idRef[VulnerabilityHandle]{ id: &t.VulnerabilityID, ref: &t.Vulnerability, } } // fillUnaffectedCPEHandles lazy loads all properties on the list of UnaffectedCPEHandles func fillUnaffectedCPEHandles(reader Reader, handles []*UnaffectedCPEHandle) error { return errors.Join( reader.attachBlobValue(toBlobables(handles)...), fillRefs(reader, handles, unaffectedCPEHandleCpeRef, cpeHandleID), fillVulnerabilityHandles(reader, handles, unaffectedCPEHandleVulnerabilityHandleRef), ) } func unaffectedCPEHandleCpeRef(t *UnaffectedCPEHandle) idRef[Cpe] { return idRef[Cpe]{ id: &t.CpeID, ref: &t.CPE, } } func unaffectedCPEHandleVulnerabilityHandleRef(t *UnaffectedCPEHandle) idRef[VulnerabilityHandle] { return idRef[VulnerabilityHandle]{ id: &t.VulnerabilityID, ref: &t.Vulnerability, } } // fillVulnerabilityHandles lazy loads vulnerability handle properties func fillVulnerabilityHandles[T any](reader Reader, handles []*T, vulnHandleRef refProvider[T, VulnerabilityHandle]) error { // fill vulnerabilities if err := fillRefs(reader, handles, vulnHandleRef, vulnerabilityHandleID); err != nil { return err } var providerRefs []ref[string, Provider] vulnHandles := make([]*VulnerabilityHandle, len(handles)) for i := range handles { vulnHandles[i] = *vulnHandleRef(handles[i]).ref providerRefs = append(providerRefs, ref[string, Provider]{ id: &vulnHandles[i].ProviderID, ref: &vulnHandles[i].Provider, }) } // then get references to them to fill the properties return errors.Join( reader.attachBlobValue(toBlobables(vulnHandles)...), reader.fillProviders(providerRefs), ) } func vulnerabilityHandleID(h *VulnerabilityHandle) ID { return h.ID } func cpeHandleID(h *Cpe) ID { return h.ID } func operatingSystemID(h *OperatingSystem) ID { return h.ID } func packageID(h *Package) ID { return h.ID } func toBlobables[T blobable](handles []T) []blobable { out := make([]blobable, len(handles)) for i := range handles { out[i] = handles[i] } return out } ================================================ FILE: grype/db/v6/import_metadata.go ================================================ package v6 import ( "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "github.com/OneOfOne/xxhash" "github.com/spf13/afero" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/schemaver" ) const ImportMetadataFileName = "import.json" type ImportMetadata struct { Digest string `json:"digest"` Source string `json:"source,omitempty"` ClientVersion string `json:"client_version"` } func ReadImportMetadata(fs afero.Fs, dir string) (*ImportMetadata, error) { checksumsFilePath := filepath.Join(dir, ImportMetadataFileName) if _, err := fs.Stat(checksumsFilePath); os.IsNotExist(err) { return nil, fmt.Errorf("no import metadata file at: %v", checksumsFilePath) } content, err := afero.ReadFile(fs, checksumsFilePath) if err != nil { return nil, fmt.Errorf("failed to read import metadata file: %w", err) } if len(content) == 0 { return nil, fmt.Errorf("no import metadata found at: %v", checksumsFilePath) } var doc ImportMetadata if err := json.Unmarshal(content, &doc); err != nil { return nil, fmt.Errorf("failed to unmarshal import metadata: %w", err) } if !strings.HasPrefix(doc.Digest, "xxh64:") { return nil, fmt.Errorf("import metadata digest is not in the expected format") } return &doc, nil } func CalculateDBDigest(fs afero.Fs, dbFilePath string) (string, error) { digest, err := file.HashFile(fs, dbFilePath, xxhash.New64()) if err != nil { return "", fmt.Errorf("failed to digest DB file: %w", err) } return fmt.Sprintf("xxh64:%s", digest), nil } func WriteImportMetadata(fs afero.Fs, dbDir, source string) (*ImportMetadata, error) { metadataFilePath := filepath.Join(dbDir, ImportMetadataFileName) f, err := fs.OpenFile(metadataFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return nil, fmt.Errorf("failed to create import metadata file: %w", err) } defer f.Close() checksums, err := CalculateDBDigest(fs, filepath.Join(dbDir, VulnerabilityDBFileName)) if err != nil { return nil, fmt.Errorf("failed to calculate checksum for DB file: %w", err) } return writeImportMetadata(f, checksums, source) } func writeImportMetadata(writer io.Writer, checksums, source string) (*ImportMetadata, error) { if checksums == "" { return nil, fmt.Errorf("checksum is required") } if !strings.HasPrefix(checksums, "xxh64:") { return nil, fmt.Errorf("checksum missing algorithm prefix") } enc := json.NewEncoder(writer) enc.SetIndent("", " ") doc := ImportMetadata{ Digest: checksums, Source: source, ClientVersion: schemaver.New(ModelVersion, Revision, Addition).String(), } return &doc, enc.Encode(doc) } ================================================ FILE: grype/db/v6/import_metadata_test.go ================================================ package v6 import ( "bytes" "encoding/json" "os" "path/filepath" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/internal/schemaver" ) func TestReadImportMetadata(t *testing.T) { tests := []struct { name string fileContent string emptyFile bool expectedErr string expectedResult *ImportMetadata }{ { name: "file does not exist", fileContent: "", expectedErr: "no import metadata", }, { name: "empty file", emptyFile: true, expectedErr: "no import metadata", }, { name: "invalid json", fileContent: "invalid json", expectedErr: "failed to unmarshal import metadata", }, { name: "missing checksum prefix", fileContent: `{"digest": "invalid", "client_version": "1.0.0"}`, expectedErr: "import metadata digest is not in the expected format", }, { name: "valid metadata", fileContent: `{"digest": "xxh64:testdigest", "source": "http://localhost:1234/archive.tar.gz", "client_version": "1.0.0"}`, expectedResult: &ImportMetadata{ Digest: "xxh64:testdigest", Source: "http://localhost:1234/archive.tar.gz", ClientVersion: "1.0.0", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() filePath := filepath.Join(dir, ImportMetadataFileName) if tt.fileContent != "" { err := os.WriteFile(filePath, []byte(tt.fileContent), 0644) require.NoError(t, err) } else if tt.emptyFile { _, err := os.Create(filePath) require.NoError(t, err) } result, err := ReadImportMetadata(afero.NewOsFs(), dir) if tt.expectedErr != "" { require.ErrorContains(t, err, tt.expectedErr) require.Nil(t, result) } else { require.NoError(t, err) require.Equal(t, tt.expectedResult, result) } }) } } func TestWriteImportMetadata(t *testing.T) { cases := []struct { name string checksum string expectedVersion string wantErr require.ErrorAssertionFunc }{ { name: "valid checksum", checksum: "xxh64:testdigest", expectedVersion: schemaver.New(ModelVersion, Revision, Addition).String(), wantErr: require.NoError, }, { name: "empty checksum", checksum: "", wantErr: require.Error, }, { name: "missing prefix", checksum: "testdigest", wantErr: require.Error, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer src := "source!" claim, err := writeImportMetadata(&buf, tc.checksum, src) tc.wantErr(t, err) if err == nil { result := buf.String() var doc ImportMetadata err := json.Unmarshal([]byte(result), &doc) require.NoError(t, err) assert.Equal(t, tc.checksum, doc.Digest) assert.Equal(t, tc.checksum, claim.Digest) assert.Equal(t, tc.expectedVersion, doc.ClientVersion) assert.Equal(t, tc.expectedVersion, claim.ClientVersion) assert.Equal(t, src, doc.Source) } }) } } func TestCalculateDBDigest(t *testing.T) { tests := []struct { name string fileContent string expectedErr string expectedDigest string }{ { name: "file does not exist", fileContent: "", expectedErr: "failed to digest DB file", }, { name: "valid file", fileContent: "testcontent", expectedDigest: "xxh64:d37ed71e4fee2ebd", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() filePath := filepath.Join(dir, VulnerabilityDBFileName) if tt.fileContent != "" { err := os.WriteFile(filePath, []byte(tt.fileContent), 0644) require.NoError(t, err) } digest, err := CalculateDBDigest(afero.NewOsFs(), filePath) if tt.expectedErr != "" { require.ErrorContains(t, err, tt.expectedErr) require.Empty(t, digest) } else { require.NoError(t, err) require.Equal(t, tt.expectedDigest, digest) } }) } } ================================================ FILE: grype/db/v6/installation/curator.go ================================================ package installation import ( "context" "errors" "fmt" "io" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/adrg/xdg" "github.com/hako/durafmt" "github.com/mholt/archives" "github.com/spf13/afero" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/clio" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/schemaver" ) const lastUpdateCheckFileName = "last_update_check" type monitor struct { *progress.AtomicStage downloadProgress completionMonitor importProgress completionMonitor hydrateProgress completionMonitor } type Config struct { DBRootDir string Debug bool // validations ValidateAge bool ValidateChecksum bool MaxAllowedBuiltAge time.Duration UpdateCheckMaxFrequency time.Duration } func DefaultConfig(id clio.Identification) Config { return Config{ DBRootDir: filepath.Join(xdg.CacheHome, id.Name, "db"), ValidateAge: true, ValidateChecksum: true, MaxAllowedBuiltAge: time.Hour * 24 * 5, // 5 days UpdateCheckMaxFrequency: 2 * time.Hour, // 2 hours } } func (c Config) DBFilePath() string { return filepath.Join(c.DBDirectoryPath(), db.VulnerabilityDBFileName) } func (c Config) DBDirectoryPath() string { return filepath.Join(c.DBRootDir, strconv.Itoa(db.ModelVersion)) } type curator struct { fs afero.Fs client distribution.Client config Config hydrator func(string) error } func NewCurator(cfg Config, downloader distribution.Client) (db.Curator, error) { return curator{ fs: afero.NewOsFs(), client: downloader, config: cfg, hydrator: db.Hydrater(), }, nil } func (c curator) Reader() (db.Reader, error) { status := c.Status() if err := status.Error; err != nil && errors.Is(err, db.ErrDBDoesNotExist) { return nil, fmt.Errorf("vulnerability database error: %w. Try 'grype db update'", err) } s, err := db.NewReader( db.Config{ DBDirPath: c.config.DBDirectoryPath(), Debug: c.config.Debug, }, ) if err != nil { return nil, err } m, err := s.GetDBMetadata() if err != nil { return nil, fmt.Errorf("unable to get vulnerability store metadata: %w", err) } var currentDBSchemaVersion *schemaver.SchemaVer if m != nil { v := schemaver.New(m.Model, m.Revision, m.Addition) currentDBSchemaVersion = &v } doRehydrate, err := isRehydrationNeeded(c.fs, c.config.DBDirectoryPath(), currentDBSchemaVersion, schemaver.New(db.ModelVersion, db.Revision, db.Addition)) if err != nil { return nil, err } if doRehydrate { if err = s.Close(); err != nil { // DB connection may be in an inconsistent state -- we cannot continue return nil, fmt.Errorf("unable to close reader before rehydration: %w", err) } mon := newMonitor() mon.Set("rehydrating DB") log.Info("rehydrating DB") // we're not changing the source of the DB, so we just want to use any existing value. // if the source is empty/does not exist, it will be empty in the new metadata. var source string im, err := db.ReadImportMetadata(c.fs, c.config.DBDirectoryPath()) if err == nil && im != nil { // ignore errors, as this is just a best-effort to get the source source = im.Source } // this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration. // this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities). // the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities. if err := c.hydrate(c.config.DBDirectoryPath(), source, mon); err != nil { log.WithFields("error", err).Warn("unable to rehydrate DB") } mon.Set("rehydrated") mon.SetCompleted() s, err = db.NewReader( db.Config{ DBDirPath: c.config.DBDirectoryPath(), Debug: c.config.Debug, }, ) if err != nil { return nil, fmt.Errorf("unable to create new reader after rehydration: %w", err) } } return s, nil } func (c curator) Status() vulnerability.ProviderStatus { dbFile := c.config.DBFilePath() d, validateErr := db.ReadDescription(dbFile) if validateErr != nil { return vulnerability.ProviderStatus{ Path: dbFile, Error: validateErr, } } if d == nil { return vulnerability.ProviderStatus{ Path: dbFile, Error: fmt.Errorf("database not found at %q", dbFile), } } validateErr = c.validateAge(d) _, checksumErr := c.validateIntegrity(d) if checksumErr != nil && c.config.ValidateChecksum { if validateErr != nil { validateErr = errors.Join(validateErr, checksumErr) } else { validateErr = checksumErr } } var source string im, readErr := db.ReadImportMetadata(c.fs, c.config.DBDirectoryPath()) if readErr == nil && im != nil { // only make a best-effort to get the source source = im.Source } return vulnerability.ProviderStatus{ Built: d.Built.Time, SchemaVersion: d.SchemaVersion.String(), From: source, Path: dbFile, Error: validateErr, } } // Delete removes the DB and metadata file for this specific schema. func (c curator) Delete() error { return c.fs.RemoveAll(c.config.DBDirectoryPath()) } // Update the existing DB, returning an indication if any action was taken. func (c curator) Update() (bool, error) { current, err := db.ReadDescription(c.config.DBFilePath()) if err != nil { // we should not warn if the DB does not exist, as this is a common first-run case... but other cases we // may care about, so warn in those cases. if !errors.Is(err, db.ErrDBDoesNotExist) { log.WithFields("error", err).Warn("unable to read current database metadata; continuing with update") } // downstream any non-existent DB should always be replaced with any best-candidate found current = nil } else { err = c.validateAge(current) if err != nil { // even if we are not allowed to check for an update, we should still attempt to update the DB if it is invalid log.WithFields("error", err).Warn("current database is invalid") current = nil } } if current != nil && !c.isUpdateCheckAllowed() { // we should not notify the user of an update check if the current configuration and state // indicates we are in a low-pass filter mode and the check frequency is too high. // this should appear to the user as if we never attempted to check for an update at all. return false, nil } update, err := c.update(current) if err != nil { return false, err } if update == nil { return false, nil } if current != nil { log.WithFields( "from", current.Built.String(), "to", update.Description.Built.String(), "version", update.Description.SchemaVersion, ).Info("updated vulnerability DB") return true, nil } log.WithFields( "version", update.Description.SchemaVersion, "built", update.Description.Built.String(), ).Info("installed new vulnerability DB") return true, nil } func (c curator) isUpdateCheckAllowed() bool { if c.config.UpdateCheckMaxFrequency == 0 { log.Trace("no max-frequency set for update check") return true } elapsed, err := c.durationSinceUpdateCheck() if err != nil { // we had an IO error (or similar) trying to read or parse the file, we should not block the update check. log.WithFields("error", err).Trace("unable to determine if update check is allowed") return true } if elapsed == nil { // there was no last check (this is a first run case), we should not block the update check. return true } return *elapsed > c.config.UpdateCheckMaxFrequency } func (c curator) update(current *db.Description) (*distribution.Archive, error) { mon := newMonitor() defer mon.SetCompleted() startTime := time.Now() mon.Set("checking for update") update, checkErr := c.client.IsUpdateAvailable(current) if checkErr != nil { // we want to continue even if we can't check for an update log.Warnf("unable to check for vulnerability database update") log.WithFields("error", checkErr).Debug("check for vulnerability update failed") } if update == nil { if checkErr == nil { // there was no update (or any issue while checking for an update) c.setLastSuccessfulUpdateCheck() } mon.Set("no update available") return nil, checkErr } log.Info("downloading new vulnerability DB") mon.Set("downloading") url, err := c.client.ResolveArchiveURL(*update) if err != nil { return nil, fmt.Errorf("unable to resolve vulnerability DB URL: %w", err) } // Ensure parent of DBRootDir exists for the download client to create a temp dir within DBRootDir // This might be redundant if DBRootDir must already exist, but good for safety. if err := os.MkdirAll(c.config.DBRootDir, 0o700); err != nil { return nil, fmt.Errorf("unable to create db root dir %s for download: %w", c.config.DBRootDir, err) } dest, err := c.client.Download(url, c.config.DBRootDir, mon.downloadProgress.Manual) if err != nil { return nil, fmt.Errorf("unable to update vulnerability database: %w", err) } log.WithFields("url", url, "time", time.Since(startTime)).Info("downloaded vulnerability DB") mon.downloadProgress.SetCompleted() if err = c.activate(dest, url, mon); err != nil { log.Warnf("Failed to activate downloaded database from %s, attempting cleanup of temporary download directory.", dest) removeAllOrLog(c.fs, dest) return nil, fmt.Errorf("unable to activate new vulnerability database: %w", err) } mon.Set("updated") // only set the last successful update check if the update was successful c.setLastSuccessfulUpdateCheck() return update, nil } func isRehydrationNeeded(fs afero.Fs, dirPath string, currentDBVersion *schemaver.SchemaVer, currentClientVersion schemaver.SchemaVer) (bool, error) { if currentDBVersion == nil { // there is no DB to rehydrate return false, nil } importMetadata, err := db.ReadImportMetadata(fs, dirPath) if err != nil { return false, fmt.Errorf("unable to read import metadata: %w", err) } clientHydrationVersion, err := schemaver.Parse(importMetadata.ClientVersion) if err != nil { return false, fmt.Errorf("unable to parse client version from import metadata: %w", err) } hydratedWithOldClient := clientHydrationVersion.LessThanOrEqualTo(*currentDBVersion) haveNewerClient := clientHydrationVersion.LessThan(currentClientVersion) doRehydrate := hydratedWithOldClient && haveNewerClient msg := "DB rehydration not needed" if doRehydrate { msg = "DB rehydration needed" } log.WithFields("clientHydrationVersion", clientHydrationVersion, "currentDBVersion", currentDBVersion, "currentClientVersion", currentClientVersion).Trace(msg) if doRehydrate { // this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration. // this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities). // the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities. return true, nil } return false, nil } func (c curator) durationSinceUpdateCheck() (*time.Duration, error) { // open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp filePath := filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName) if _, err := c.fs.Stat(filePath); os.IsNotExist(err) { log.Trace("first-run of DB update") return nil, nil } fh, err := c.fs.OpenFile(filePath, os.O_RDONLY, 0) if err != nil { return nil, fmt.Errorf("unable to read last update check timestamp: %w", err) } defer log.CloseAndLogError(fh, filePath) // read and parse rfc3339 timestamp var lastCheckStr string _, err = fmt.Fscanf(fh, "%s", &lastCheckStr) if err != nil { return nil, fmt.Errorf("unable to read last update check timestamp: %w", err) } lastCheck, err := time.Parse(time.RFC3339, lastCheckStr) if err != nil { return nil, fmt.Errorf("unable to parse last update check timestamp: %w", err) } if lastCheck.IsZero() { return nil, fmt.Errorf("empty update check timestamp") } elapsed := time.Since(lastCheck) return &elapsed, nil } func (c curator) setLastSuccessfulUpdateCheck() { // note: we should always assume the DB dir actually exists, otherwise let this operation fail (since having a DB // is a prerequisite for a successful update). filePath := filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName) fh, err := c.fs.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { log.WithFields("error", err).Trace("unable to write last update check timestamp") return } defer log.CloseAndLogError(fh, filePath) _, _ = fmt.Fprintf(fh, "%s", time.Now().UTC().Format(time.RFC3339)) } // Import takes a DB file path, archive file path, or URL and imports it into the final DB location. func (c curator) Import(reference string) error { mon := newMonitor() mon.Set("preparing") defer mon.SetCompleted() if err := os.MkdirAll(c.config.DBRootDir, 0o700); err != nil { return fmt.Errorf("unable to create db root dir: %w", err) } var tempDir, url string if isURL(reference) { log.Info("downloading new vulnerability DB") mon.Set("downloading") var err error tempDir, err = c.client.Download(reference, c.config.DBRootDir, mon.downloadProgress.Manual) if err != nil { return fmt.Errorf("unable to update vulnerability database: %w", err) } url = reference } else { // note: the temp directory is persisted upon download/validation/activation failure to allow for investigation var err error tempDir, err = os.MkdirTemp(c.config.DBRootDir, fmt.Sprintf("tmp-v%v-import", db.ModelVersion)) if err != nil { return fmt.Errorf("unable to create db import temp dir: %w", err) } url = "manual import" if strings.HasSuffix(reference, ".db") { // this is a raw DB file, copy it to the temp dir log.Trace("copying DB") if err := file.CopyFile(afero.NewOsFs(), reference, filepath.Join(tempDir, db.VulnerabilityDBFileName)); err != nil { return fmt.Errorf("unable to copy DB file: %w", err) } } else { // assume it is an archive log.Info("unarchiving DB") err := unarchive(reference, tempDir) if err != nil { return err } } } mon.downloadProgress.SetCompleted() if err := c.activate(tempDir, url, mon); err != nil { removeAllOrLog(c.fs, tempDir) return err } mon.Set("imported") return nil } var urlPrefixPattern = regexp.MustCompile("^[a-zA-Z]+://") func isURL(reference string) bool { return urlPrefixPattern.MatchString(reference) } // activate swaps over the downloaded db to the application directory, calculates the checksum, and records the checksums to a file. func (c curator) activate(dbDirPath, url string, mon monitor) error { defer mon.SetCompleted() startTime := time.Now() if err := c.hydrate(dbDirPath, url, mon); err != nil { return fmt.Errorf("failed to hydrate database: %w", err) } log.WithFields("time", time.Since(startTime)).Trace("hydrated db") startTime = time.Now() defer func() { log.WithFields("time", time.Since(startTime)).Trace("replaced db") }() mon.Set("activating") return c.replaceDB(dbDirPath) } func (c curator) hydrate(dbDirPath, from string, mon monitor) error { if c.hydrator != nil { mon.Set("hydrating") if err := c.hydrator(dbDirPath); err != nil { return err } } mon.hydrateProgress.SetCompleted() mon.Set("hashing") doc, err := db.WriteImportMetadata(c.fs, dbDirPath, from) if err != nil { return fmt.Errorf("failed to write checksums file: %w", err) } log.WithFields("digest", doc.Digest).Trace("captured DB digest") return nil } // replaceDB swaps over to using the given path. func (c curator) replaceDB(dbDirPath string) error { dbDir := c.config.DBDirectoryPath() _, err := c.fs.Stat(dbDir) if !os.IsNotExist(err) { // remove any previous databases err = c.Delete() if err != nil { return fmt.Errorf("failed to purge existing database: %w", err) } } // ensure parent db directory exists if err = c.fs.MkdirAll(filepath.Dir(dbDir), 0o700); err != nil { return fmt.Errorf("unable to create db parent directory: %w", err) } // activate the new db cache by moving the temp dir to final location // the rename should be safe because the temp dir is under GRYPE_DB_CACHE_DIR // and so on the same filesystem as the final location err = c.fs.Rename(dbDirPath, dbDir) if err != nil { err = fmt.Errorf("failed to move database directory to activate: %w", err) } log.WithFields("from", dbDirPath, "to", dbDir, "error", err).Debug("moved database directory to activate") return err } // validateIntegrity checks that the disk checksum still matches the db payload func (c curator) validateIntegrity(description *db.Description) (string, error) { dbFilePath := c.config.DBFilePath() // check that the disk checksum still matches the db payload if description == nil { return "", fmt.Errorf("database not found: %s", dbFilePath) } if description.SchemaVersion.Model != db.ModelVersion { return "", fmt.Errorf("unsupported database version: have=%d want=%d", description.SchemaVersion.Model, db.ModelVersion) } if _, err := c.fs.Stat(dbFilePath); err != nil { if os.IsNotExist(err) { return "", fmt.Errorf("database does not exist: %s", dbFilePath) } return "", fmt.Errorf("failed to access database file: %w", err) } importMetadata, err := db.ReadImportMetadata(c.fs, filepath.Dir(dbFilePath)) if err != nil { return "", err } valid, actualHash, err := file.ValidateByHash(c.fs, dbFilePath, importMetadata.Digest) if err != nil { return actualHash, err } if !valid { return actualHash, fmt.Errorf("bad db checksum (%s): %q vs %q", dbFilePath, importMetadata.Digest, actualHash) } return actualHash, nil } // validateAge ensures the vulnerability database has not passed // the max allowed age, calculated from the time it was built until now. func (c curator) validateAge(m *db.Description) error { if m == nil { return fmt.Errorf("no metadata to validate") } if !c.config.ValidateAge { return nil } // built time is defined in UTC, // we should compare it against UTC now := time.Now().UTC() age := now.Sub(m.Built.Time) if age > c.config.MaxAllowedBuiltAge { return fmt.Errorf("the vulnerability database was built %s ago (max allowed age is %s)", durafmt.ParseShort(age), durafmt.ParseShort(c.config.MaxAllowedBuiltAge)) } return nil } func removeAllOrLog(fs afero.Fs, dir string) { if err := fs.RemoveAll(dir); err != nil { log.WithFields("error", err).Warnf("failed to remove path %q", dir) } } func unarchive(source, destination string) error { sourceFile, err := os.Open(source) if err != nil { return err } defer sourceFile.Close() format, stream, err := archives.Identify(context.Background(), source, sourceFile) if err != nil { return err } extractor, ok := format.(archives.Extractor) if !ok { return fmt.Errorf("unable to extract DB file, format not supported: %s", source) } root, err := os.OpenRoot(destination) if err != nil { return err } visitor := func(_ context.Context, file archives.FileInfo) error { if file.IsDir() || file.LinkTarget != "" { return nil } fileReader, err := file.Open() if err != nil { return err } defer fileReader.Close() filename := filepath.Clean(file.NameInArchive) outputFile, err := root.Create(filename) if err != nil { return err } defer outputFile.Close() _, err = io.Copy(outputFile, fileReader) return err } return extractor.Extract(context.Background(), stream, visitor) } func newMonitor() monitor { // let consumers know of a monitorable event (download + import stages) importProgress := progress.NewManual(1) stage := progress.NewAtomicStage("") downloadProgress := progress.NewManual(1) hydrateProgress := progress.NewManual(1) aggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, hydrateProgress, importProgress) bus.Publish(partybus.Event{ Type: event.UpdateVulnerabilityDatabase, Value: progress.StagedProgressable(&struct { progress.Stager progress.Progressable }{ Stager: progress.Stager(stage), Progressable: progress.Progressable(aggregateProgress), }), }) return monitor{ AtomicStage: stage, downloadProgress: completionMonitor{downloadProgress}, importProgress: completionMonitor{importProgress}, hydrateProgress: completionMonitor{hydrateProgress}, } } func (m monitor) SetCompleted() { m.downloadProgress.SetCompleted() m.importProgress.SetCompleted() m.hydrateProgress.SetCompleted() } // completionMonitor is a progressable that, when SetComplete() is called, will set the progress to the total size type completionMonitor struct { *progress.Manual } func (m completionMonitor) SetCompleted() { m.Set(m.Size()) m.Manual.SetCompleted() } ================================================ FILE: grype/db/v6/installation/curator_test.go ================================================ package installation import ( "encoding/json" "errors" "os" "path/filepath" "testing" "time" "github.com/mholt/archives" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/wagoodman/go-progress" "github.com/anchore/clio" db "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/internal/schemaver" ) type mockClient struct { mock.Mock } func (m *mockClient) IsUpdateAvailable(current *db.Description) (*distribution.Archive, error) { args := m.Called(current) err := args.Error(1) if err != nil { return nil, err } return args.Get(0).(*distribution.Archive), nil } func (m *mockClient) ResolveArchiveURL(_ distribution.Archive) (string, error) { return "http://localhost/archive.tar.zst", nil } func (m *mockClient) Download(url, dest string, downloadProgress *progress.Manual) (string, error) { args := m.Called(url, dest, downloadProgress) return args.String(0), args.Error(1) } func (m *mockClient) Latest() (*distribution.LatestDocument, error) { args := m.Called() return args.Get(0).(*distribution.LatestDocument), args.Error(1) } func newTestCurator(t *testing.T) curator { tempDir := t.TempDir() cfg := testConfig() cfg.DBRootDir = tempDir ci, err := NewCurator(cfg, new(mockClient)) require.NoError(t, err) c := ci.(curator) return c } type setupConfig struct { workingUpdate bool } type setupOption func(*setupConfig) func withWorkingUpdateIntegrations() setupOption { return func(c *setupConfig) { c.workingUpdate = true } } func setupCuratorForUpdate(t *testing.T, opts ...setupOption) curator { cfg := setupConfig{} for _, o := range opts { o(&cfg) } c := newTestCurator(t) dbDir := c.config.DBDirectoryPath() stageConfig := Config{DBRootDir: filepath.Join(c.config.DBRootDir, "staged")} stageDir := stageConfig.DBDirectoryPath() // populate metadata into the downloaded dir oldDescription := db.Description{ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Built: db.Time{Time: time.Now().Add(-48 * time.Hour)}, } writeTestDB(t, c.fs, dbDir) newDescription := oldDescription newDescription.Built = db.Time{Time: time.Now()} writeTestDB(t, c.fs, stageDir) writeTestDescriptionToDB(t, dbDir, oldDescription) writeTestDescriptionToDB(t, stageDir, newDescription) if cfg.workingUpdate { mc := c.client.(*mockClient) // ensure the update "works" mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil) mc.On("Download", mock.Anything, mock.Anything, mock.Anything).Return(stageDir, nil) } return c } func writeTestDescriptionToDB(t *testing.T, dir string, desc db.Description) string { c := db.Config{DBDirPath: dir} d, err := db.NewLowLevelDB(c.DBFilePath(), false, true, true) require.NoError(t, err) if err := d.Where("true").Delete(&db.DBMetadata{}).Error; err != nil { t.Fatalf("failed to delete existing DB metadata record: %v", err) } require.NotEmpty(t, desc.SchemaVersion.Model) require.NotEmpty(t, desc.SchemaVersion.String()) ts := time.Now().UTC() instance := &db.DBMetadata{ BuildTimestamp: &ts, Model: desc.SchemaVersion.Model, Revision: desc.SchemaVersion.Revision, Addition: desc.SchemaVersion.Revision, } require.NoError(t, d.Create(instance).Error) require.NoError(t, d.Exec("VACUUM").Error) digest, err := db.CalculateDBDigest(afero.NewOsFs(), c.DBFilePath()) require.NoError(t, err) writeTestImportMetadata(t, afero.NewOsFs(), dir, digest) return digest } func writeTestImportMetadata(t *testing.T, fs afero.Fs, dir string, checksums string) { writeTestImportMetadataWithCustomVersion(t, fs, dir, checksums, schemaver.New(db.ModelVersion, db.Revision, db.Addition).String()) } func writeTestImportMetadataWithCustomVersion(t *testing.T, fs afero.Fs, dir string, checksums string, ver string) { require.NoError(t, fs.MkdirAll(dir, 0755)) metadataFilePath := filepath.Join(dir, db.ImportMetadataFileName) writer, err := afero.NewOsFs().Create(metadataFilePath) require.NoError(t, err) defer func() { _ = writer.Close() }() enc := json.NewEncoder(writer) enc.SetIndent("", " ") doc := db.ImportMetadata{ Digest: checksums, ClientVersion: ver, } require.NoError(t, enc.Encode(doc)) } func writeTestDB(t *testing.T, fs afero.Fs, dir string) string { require.NoError(t, fs.MkdirAll(dir, 0755)) rw, err := db.NewWriter(db.Config{ DBDirPath: dir, }) require.NoError(t, err) require.NoError(t, rw.SetDBMetadata()) require.NoError(t, rw.Close()) doc, err := db.WriteImportMetadata(fs, dir, "source") require.NoError(t, err) require.NotNil(t, doc) return doc.Digest } func TestCurator_Update(t *testing.T) { t.Run("happy path: successful update", func(t *testing.T) { c := setupCuratorForUpdate(t, withWorkingUpdateIntegrations()) mc := c.client.(*mockClient) // nop hydrator, assert error if NOT called hydrateCalled := false c.hydrator = func(string) error { hydrateCalled = true return nil } updated, err := c.Update() require.NoError(t, err) require.True(t, updated) require.FileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) mc.AssertExpectations(t) assert.True(t, hydrateCalled, "expected hydrator to be called") }) t.Run("error checking for updates", func(t *testing.T) { c := setupCuratorForUpdate(t) mc := c.client.(*mockClient) mc.On("IsUpdateAvailable", mock.Anything).Return(nil, errors.New("check failed")) updated, err := c.Update() require.Error(t, err) require.False(t, updated) require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) mc.AssertExpectations(t) }) t.Run("error during download", func(t *testing.T) { c := setupCuratorForUpdate(t) mc := c.client.(*mockClient) mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil) mc.On("Download", mock.Anything, mock.Anything, mock.Anything).Return("", errors.New("download failed")) updated, err := c.Update() require.ErrorContains(t, err, "download failed") require.False(t, updated) require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) mc.AssertExpectations(t) }) t.Run("error during activation: cannot move dir", func(t *testing.T) { c := setupCuratorForUpdate(t, withWorkingUpdateIntegrations()) mc := c.client.(*mockClient) // nop hydrator c.hydrator = nil // simulate not being able to move the staged dir to the db dir c.fs = afero.NewReadOnlyFs(c.fs) updated, err := c.Update() require.ErrorContains(t, err, "operation not permitted") require.False(t, updated) require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) mc.AssertExpectations(t) }) } func TestCurator_IsUpdateCheckAllowed(t *testing.T) { newCurator := func(t *testing.T) curator { tempDir := t.TempDir() cfg := testConfig() cfg.UpdateCheckMaxFrequency = 10 * time.Minute cfg.DBRootDir = tempDir ci, err := NewCurator(cfg, nil) require.NoError(t, err) c := ci.(curator) return c } writeLastCheckContents := func(t *testing.T, cfg Config, contents string) { require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755)) p := filepath.Join(cfg.DBDirectoryPath(), lastUpdateCheckFileName) err := os.WriteFile(p, []byte(contents), 0644) require.NoError(t, err) } writeLastCheckTime := func(t *testing.T, cfg Config, lastCheckTime time.Time) { writeLastCheckContents(t, cfg, lastCheckTime.Format(time.RFC3339)) } t.Run("first run check (no last check file)", func(t *testing.T) { c := newCurator(t) require.True(t, c.isUpdateCheckAllowed()) }) t.Run("check not allowed due to frequency", func(t *testing.T) { c := newCurator(t) writeLastCheckTime(t, c.config, time.Now().Add(-5*time.Minute)) require.False(t, c.isUpdateCheckAllowed()) }) t.Run("check allowed after the frequency period", func(t *testing.T) { c := newCurator(t) writeLastCheckTime(t, c.config, time.Now().Add(-20*time.Minute)) require.True(t, c.isUpdateCheckAllowed()) }) t.Run("error reading last check file", func(t *testing.T) { c := newCurator(t) // simulate a situation where the last check file exists but is corrupted writeLastCheckContents(t, c.config, "invalid timestamp") allowed := c.isUpdateCheckAllowed() require.True(t, allowed) // should return true since an error is encountered }) } func TestCurator_DurationSinceUpdateCheck(t *testing.T) { newCurator := func(t *testing.T) curator { tempDir := t.TempDir() cfg := testConfig() cfg.DBRootDir = tempDir ci, err := NewCurator(cfg, nil) require.NoError(t, err) c := ci.(curator) return c } writeLastCheckContents := func(t *testing.T, cfg Config, contents string) { require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755)) p := filepath.Join(cfg.DBDirectoryPath(), lastUpdateCheckFileName) err := os.WriteFile(p, []byte(contents), 0644) require.NoError(t, err) } t.Run("no last check file", func(t *testing.T) { c := newCurator(t) elapsed, err := c.durationSinceUpdateCheck() require.NoError(t, err) require.Nil(t, elapsed) // should be nil since no file exists }) t.Run("valid last check file", func(t *testing.T) { c := newCurator(t) writeLastCheckContents(t, c.config, time.Now().Add(-5*time.Minute).Format(time.RFC3339)) elapsed, err := c.durationSinceUpdateCheck() require.NoError(t, err) require.NotNil(t, elapsed) require.True(t, *elapsed >= 5*time.Minute) // should be at least 5 minutes }) t.Run("malformed last check file", func(t *testing.T) { c := newCurator(t) writeLastCheckContents(t, c.config, "invalid timestamp") _, err := c.durationSinceUpdateCheck() require.Error(t, err) require.Contains(t, err.Error(), "unable to parse last update check timestamp") }) } func TestCurator_SetLastSuccessfulUpdateCheck(t *testing.T) { newCurator := func(t *testing.T) curator { tempDir := t.TempDir() cfg := testConfig() cfg.DBRootDir = tempDir ci, err := NewCurator(cfg, nil) require.NoError(t, err) c := ci.(curator) require.NoError(t, c.fs.MkdirAll(c.config.DBDirectoryPath(), 0755)) return c } t.Run("set last successful update check", func(t *testing.T) { c := newCurator(t) c.setLastSuccessfulUpdateCheck() data, err := afero.ReadFile(c.fs, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) require.NoError(t, err) lastCheckTime, err := time.Parse(time.RFC3339, string(data)) require.NoError(t, err) require.WithinDuration(t, time.Now().UTC(), lastCheckTime, time.Second) }) t.Run("error writing last successful update check", func(t *testing.T) { c := newCurator(t) // make the file system read-only to simulate a write error readonlyFs := afero.NewReadOnlyFs(c.fs) c.fs = readonlyFs c.setLastSuccessfulUpdateCheck() require.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) }) t.Run("ensure last successful update check file is created", func(t *testing.T) { c := newCurator(t) c.setLastSuccessfulUpdateCheck() require.FileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)) }) } func TestCurator_validateAge(t *testing.T) { newCurator := func(t *testing.T) curator { tempDir := t.TempDir() cfg := testConfig() cfg.DBRootDir = tempDir cfg.MaxAllowedBuiltAge = 48 * time.Hour // set max age to 48 hours ci, err := NewCurator(cfg, new(mockClient)) require.NoError(t, err) return ci.(curator) } hoursAgo := func(h int) db.Time { return db.Time{Time: time.Now().UTC().Add(-time.Duration(h) * time.Hour)} } tests := []struct { name string description *db.Description wantErr require.ErrorAssertionFunc modifyConfig func(*Config) }{ { name: "valid metadata within age limit", description: &db.Description{ Built: hoursAgo(24), }, }, { name: "stale metadata exactly at age limit", description: &db.Description{ Built: hoursAgo(48), }, wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.ErrorContains(t, err, "the vulnerability database was built") }, }, { name: "stale metadata", description: &db.Description{ Built: hoursAgo(50), }, wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.ErrorContains(t, err, "the vulnerability database was built") }, }, { name: "no metadata", description: nil, wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.ErrorContains(t, err, "no metadata to validate") }, }, { name: "age validation disabled", description: &db.Description{ Built: hoursAgo(50), }, modifyConfig: func(cfg *Config) { cfg.ValidateAge = false }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } c := newCurator(t) if tt.modifyConfig != nil { tt.modifyConfig(&c.config) } err := c.validateAge(tt.description) tt.wantErr(t, err) }) } } func TestCurator_validateIntegrity(t *testing.T) { newCurator := func(t *testing.T) (curator, *db.Description) { tempDir := t.TempDir() cfg := testConfig() cfg.DBRootDir = tempDir require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755)) sw := setupTestDB(t, cfg.DBDirectoryPath()) require.NoError(t, sw.SetDBMetadata()) require.NoError(t, sw.Close()) s := setupReadOnlyTestDB(t, cfg.DBDirectoryPath()) // assume that we already have a valid checksum file digest, err := db.CalculateDBDigest(afero.NewOsFs(), cfg.DBFilePath()) require.NoError(t, err) writeTestImportMetadata(t, afero.NewOsFs(), cfg.DBDirectoryPath(), digest) ci, err := NewCurator(cfg, new(mockClient)) require.NoError(t, err) m, err := s.GetDBMetadata() require.NoError(t, err) return ci.(curator), db.DescriptionFromMetadata(m) } t.Run("valid metadata with correct checksum", func(t *testing.T) { c, d := newCurator(t) digest, err := c.validateIntegrity(d) require.NoError(t, err) require.NotEmpty(t, digest) }) t.Run("db does not exist", func(t *testing.T) { c, d := newCurator(t) require.NoError(t, os.Remove(c.config.DBFilePath())) _, err := c.validateIntegrity(d) require.ErrorContains(t, err, "database does not exist") }) t.Run("import metadata file does not exist", func(t *testing.T) { c, d := newCurator(t) dbDir := c.config.DBDirectoryPath() require.NoError(t, os.Remove(filepath.Join(dbDir, db.ImportMetadataFileName))) _, err := c.validateIntegrity(d) require.ErrorContains(t, err, "no import metadata") }) t.Run("invalid checksum", func(t *testing.T) { c, d := newCurator(t) dbDir := c.config.DBDirectoryPath() writeTestImportMetadata(t, c.fs, dbDir, "xxh64:invalidchecksum") _, err := c.validateIntegrity(d) require.ErrorContains(t, err, "bad db checksum") }) t.Run("unsupported database version", func(t *testing.T) { c, d := newCurator(t) d.SchemaVersion = schemaver.New(db.ModelVersion-1, 0, 0) _, err := c.validateIntegrity(d) require.ErrorContains(t, err, "unsupported database version") }) } func TestReplaceDB(t *testing.T) { cases := []struct { name string config Config expected map[string]string // expected file name to content mapping in the DB dir init func(t *testing.T, dir string, dbDir string) afero.Fs wantErr require.ErrorAssertionFunc verify func(t *testing.T, fs afero.Fs, config Config, expected map[string]string) }{ { name: "replace non-existent DB", config: Config{ DBRootDir: "/test", }, expected: map[string]string{ "file.txt": "new content", }, init: func(t *testing.T, dir string, dbDir string) afero.Fs { fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) require.NoError(t, fs.MkdirAll(dir, 0700)) require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("new content"), 0644)) return fs }, }, { name: "replace existing DB", config: Config{ DBRootDir: "/test", }, expected: map[string]string{ "new_file.txt": "new content", }, init: func(t *testing.T, dir string, dbDir string) afero.Fs { fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) require.NoError(t, fs.MkdirAll(dbDir, 0700)) require.NoError(t, afero.WriteFile(fs, filepath.Join(dbDir, "old_file.txt"), []byte("old content"), 0644)) require.NoError(t, fs.MkdirAll(dir, 0700)) require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "new_file.txt"), []byte("new content"), 0644)) return fs }, }, { name: "non-existent parent dir creation", config: Config{ DBRootDir: "/dir/does/not/exist/db3", }, expected: map[string]string{ "file.txt": "new content", }, init: func(t *testing.T, dir string, dbDir string) afero.Fs { fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) require.NoError(t, fs.MkdirAll(dir, 0700)) require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("new content"), 0644)) return fs }, }, { name: "error during rename", config: Config{ DBRootDir: "/test", }, expected: nil, // no files expected since operation fails init: func(t *testing.T, dir string, dbDir string) afero.Fs { fs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir()) require.NoError(t, fs.MkdirAll(dir, 0700)) require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, "file.txt"), []byte("content"), 0644)) return afero.NewReadOnlyFs(fs) }, wantErr: require.Error, verify: func(t *testing.T, fs afero.Fs, config Config, expected map[string]string) { _, err := fs.Stat(config.DBDirectoryPath()) require.Error(t, err) }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } dbDir := tc.config.DBDirectoryPath() candidateDir := "/temp/db" fs := tc.init(t, candidateDir, dbDir) c := curator{ fs: fs, config: tc.config, } err := c.replaceDB(candidateDir) tc.wantErr(t, err) if tc.verify != nil { tc.verify(t, fs, tc.config, tc.expected) } if err != nil { return } for fileName, expectedContent := range tc.expected { filePath := filepath.Join(tc.config.DBDirectoryPath(), fileName) actualContent, err := afero.ReadFile(fs, filePath) assert.NoError(t, err) assert.Equal(t, expectedContent, string(actualContent)) } }) } } func Test_isRehydrationNeeded(t *testing.T) { tests := []struct { name string currentDBVersion schemaver.SchemaVer hydrationClientVer schemaver.SchemaVer currentClientVer schemaver.SchemaVer expectedResult bool expectedErr string }{ { name: "no database exists", currentDBVersion: schemaver.SchemaVer{}, currentClientVer: schemaver.New(6, 2, 0), expectedResult: false, }, { name: "no import metadata exists", currentDBVersion: schemaver.New(6, 0, 0), currentClientVer: schemaver.New(6, 2, 0), expectedErr: "unable to read import metadata", expectedResult: false, }, { name: "invalid client version in metadata", currentDBVersion: schemaver.New(6, 0, 0), hydrationClientVer: schemaver.SchemaVer{-19, 0, 0}, currentClientVer: schemaver.New(6, 2, 0), expectedResult: false, expectedErr: "unable to parse client version from import metadata", }, { name: "rehydration needed", currentDBVersion: schemaver.New(6, 0, 1), hydrationClientVer: schemaver.New(6, 0, 0), currentClientVer: schemaver.New(6, 0, 2), expectedResult: true, }, { name: "no rehydration needed - client version equals current client version", currentDBVersion: schemaver.New(6, 0, 0), hydrationClientVer: schemaver.New(6, 2, 0), currentClientVer: schemaver.New(6, 2, 0), expectedResult: false, }, { name: "no rehydration needed - client version greater than current client version", currentDBVersion: schemaver.New(6, 0, 0), hydrationClientVer: schemaver.New(6, 3, 0), currentClientVer: schemaver.New(6, 2, 0), expectedResult: false, }, { // there are cases where new features will result in new columns, thus an old client downloading and hydrating // a DB should function, however, when the new client is downloaded it should trigger at least a rehydration // of the existing DB (in cases where the new DB is not available for download yet). name: "rehydration needed - we have a new client version, with an old DB version", currentDBVersion: schemaver.New(6, 0, 2), hydrationClientVer: schemaver.New(6, 0, 2), currentClientVer: schemaver.New(6, 0, 3), expectedResult: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fs := afero.NewOsFs() testDir := t.TempDir() if tt.hydrationClientVer.Model != 0 { writeTestImportMetadataWithCustomVersion(t, fs, testDir, "xxh64:something", tt.hydrationClientVer.String()) } var dbVersion *schemaver.SchemaVer if tt.currentDBVersion.Model != 0 { dbVersion = &tt.currentDBVersion } result, err := isRehydrationNeeded(fs, testDir, dbVersion, tt.currentClientVer) if tt.expectedErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedErr) } else { require.NoError(t, err) assert.Equal(t, tt.expectedResult, result) } }) } } func TestCurator_Update_UsesDBRootDirForDownloadTempBase(t *testing.T) { c := newTestCurator(t) // This sets up c.fs as afero.NewOsFs() rooted in t.TempDir() mc := c.client.(*mockClient) // This is the path that the mocked Download method will return. // It simulates a temporary directory created by the download client within DBRootDir. expectedDownloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-downloaded-db-content-123") // Pre-create this directory and make it look like a valid DB source for the hydrator and replaceDB. require.NoError(t, c.fs.MkdirAll(expectedDownloadedContentPath, 0755)) // Write minimal valid DB metadata so that hydration/activation can proceed far enough. // Using existing helpers to create a semblance of a DB. writeTestDB(t, c.fs, expectedDownloadedContentPath) // This creates a basic DB file and import metadata. // Mock client responses mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil) // CRUCIAL ASSERTION: // Verify that Download is called with c.config.DBRootDir as its second argument (baseDirForTemp). // It will return the expectedDownloadedContentPath, simulating successful download and extraction. mc.On("Download", mock.Anything, c.config.DBRootDir, mock.Anything).Return(expectedDownloadedContentPath, nil) hydrateCalled := false c.hydrator = func(path string) error { // Ensure hydrator is called with the path returned by Download assert.Equal(t, expectedDownloadedContentPath, path, "hydrator called with incorrect path") hydrateCalled = true return nil // Simulate successful hydration } // Call Update to trigger the download and activation sequence updated, err := c.Update() // Assertions require.NoError(t, err, "Update should succeed") require.True(t, updated, "Update should report true") mc.AssertExpectations(t) // Verifies that Download was called with the expected arguments assert.True(t, hydrateCalled, "expected hydrator to be called") // Check if the DB was "activated" (i.e., renamed) finalDBPath := c.config.DBDirectoryPath() _, err = c.fs.Stat(finalDBPath) require.NoError(t, err, "final DB directory should exist after successful update") // And the temporary downloaded content path should no longer exist as it was renamed _, err = c.fs.Stat(expectedDownloadedContentPath) require.True(t, os.IsNotExist(err), "temporary download path should not exist after rename") } func TestCurator_Update_CleansUpDownloadDirOnActivationFailure(t *testing.T) { c := newTestCurator(t) // Sets up c.fs as afero.NewOsFs() rooted in t.TempDir() mc := c.client.(*mockClient) // This is the path that the mocked Download method will return. // This directory should be cleaned up if activation fails. downloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-download-to-be-cleaned-up") // Simulate the download client successfully creating this directory. require.NoError(t, c.fs.MkdirAll(downloadedContentPath, 0755)) // Optionally, put a dummy file inside to make the cleanup more tangible. require.NoError(t, afero.WriteFile(c.fs, filepath.Join(downloadedContentPath, "dummy_file.txt"), []byte("test data"), 0644)) // Mock client responses mc.On("IsUpdateAvailable", mock.Anything).Return(&distribution.Archive{}, nil) // Download is called with DBRootDir as base, and returns the path to the (simulated) downloaded content. mc.On("Download", mock.Anything, c.config.DBRootDir, mock.Anything).Return(downloadedContentPath, nil) // Configure the hydrator to fail, which will cause c.activate() to fail. expectedHydrationError := "simulated hydration failure" c.hydrator = func(path string) error { assert.Equal(t, downloadedContentPath, path, "hydrator called with incorrect path") return errors.New(expectedHydrationError) } // Call Update, expecting it to fail during activation. updated, err := c.Update() // Assertions require.Error(t, err, "Update should fail due to activation error") require.Contains(t, err.Error(), expectedHydrationError, "Error message should reflect hydration failure") require.False(t, updated, "Update should report false on failure") mc.AssertExpectations(t) // Verifies Download was called as expected. // CRUCIAL ASSERTION: // Verify that the temporary download directory was cleaned up. _, statErr := c.fs.Stat(downloadedContentPath) require.True(t, os.IsNotExist(statErr), "expected temporary download directory to be cleaned up after activation failure") } // Test for the Import path (URL case) - very similar to the Update tests func TestCurator_Import_URL_UsesDBRootDirForDownloadTempBaseAndCleansUp(t *testing.T) { t.Run("successful import from URL", func(t *testing.T) { c := newTestCurator(t) mc := c.client.(*mockClient) importURL := "http://localhost/some/db.tar.gz" expectedDownloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-imported-db-content-url") require.NoError(t, c.fs.MkdirAll(expectedDownloadedContentPath, 0755)) writeTestDB(t, c.fs, expectedDownloadedContentPath) mc.On("Download", importURL, c.config.DBRootDir, mock.Anything).Return(expectedDownloadedContentPath, nil) hydrateCalled := false c.hydrator = func(path string) error { assert.Equal(t, expectedDownloadedContentPath, path) hydrateCalled = true return nil } err := c.Import(importURL) require.NoError(t, err) mc.AssertExpectations(t) assert.True(t, hydrateCalled) _, err = c.fs.Stat(c.config.DBDirectoryPath()) require.NoError(t, err, "final DB directory should exist") _, err = c.fs.Stat(expectedDownloadedContentPath) require.True(t, os.IsNotExist(err), "temp import path should not exist after rename") }) t.Run("import from URL fails activation", func(t *testing.T) { c := newTestCurator(t) mc := c.client.(*mockClient) importURL := "http://localhost/some/other/db.tar.gz" downloadedContentPath := filepath.Join(c.config.DBRootDir, "temp-imported-to-cleanup-url") require.NoError(t, c.fs.MkdirAll(downloadedContentPath, 0755)) require.NoError(t, afero.WriteFile(c.fs, filepath.Join(downloadedContentPath, "dummy.txt"), []byte("test"), 0644)) mc.On("Download", importURL, c.config.DBRootDir, mock.Anything).Return(downloadedContentPath, nil) expectedHydrationError := "simulated hydration failure for import" c.hydrator = func(path string) error { return errors.New(expectedHydrationError) } err := c.Import(importURL) require.Error(t, err) require.Contains(t, err.Error(), expectedHydrationError) mc.AssertExpectations(t) _, statErr := c.fs.Stat(downloadedContentPath) require.True(t, os.IsNotExist(statErr), "expected temp import directory to be cleaned up") }) } func Test_unarchive(t *testing.T) { testFile := filepath.Join(t.TempDir(), "vulnerability.db") f, err := os.Create(testFile) require.NoError(t, err) f.Close() files, err := archives.FilesFromDisk(t.Context(), nil, map[string]string{ testFile: "", }) require.NoError(t, err) source := filepath.Join(t.TempDir(), "archive.tar.zst") out, err := os.Create(source) require.NoError(t, err) format := archives.CompressedArchive{ Compression: archives.Zstd{}, Archival: archives.Tar{}, } err = format.Archive(t.Context(), out, files) require.NoError(t, err) destination := t.TempDir() err = unarchive(source, destination) require.NoError(t, err) expectFile := filepath.Join(destination, "vulnerability.db") require.FileExists(t, expectFile) } func setupTestDB(t *testing.T, dbDir string) db.ReadWriter { s, err := db.NewWriter(db.Config{ DBDirPath: dbDir, }) require.NoError(t, err) return s } func setupReadOnlyTestDB(t *testing.T, dbDir string) db.Reader { s, err := db.NewReader(db.Config{ DBDirPath: dbDir, }) require.NoError(t, err) return s } func testConfig() Config { return DefaultConfig(clio.Identification{ Name: "grype-test", }) } ================================================ FILE: grype/db/v6/log_dropped.go ================================================ package v6 import ( "github.com/anchore/go-logger" "github.com/anchore/grype/internal/log" ) // logDroppedVulnerability is a hook called when vulnerabilities are dropped from consideration in a vulnerability Provider, // this offers a convenient location to set a breakpoint // //go:noinline func logDroppedVulnerability(vuln string, reason any, fields logger.Fields) { fields["reason"] = reason fields["vulnerability"] = vuln log.WithFields(fields).Trace("dropped vulnerability") } ================================================ FILE: grype/db/v6/models.go ================================================ package v6 import ( "encoding/json" "errors" "fmt" "strings" "time" "github.com/OneOfOne/xxhash" "gorm.io/gorm" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/schemaver" ) var ( // ensure that the generic packageHandleStore will function when type asserting _ blobable = (*packageHandle)(nil) _ blobable = (*AffectedPackageHandle)(nil) _ blobable = (*UnaffectedPackageHandle)(nil) _ packageHandleAccessor = (*AffectedPackageHandle)(nil) _ packageHandleAccessor = (*UnaffectedPackageHandle)(nil) // ensure that the generic cpeHandleStore will function when type asserting _ blobable = (*cpeHandle)(nil) _ blobable = (*AffectedCPEHandle)(nil) _ blobable = (*UnaffectedCPEHandle)(nil) _ cpeHandleAccessor = (*AffectedCPEHandle)(nil) _ cpeHandleAccessor = (*UnaffectedCPEHandle)(nil) ) func Models() []any { return []any{ // core data store &Blob{}, // non-domain info &DBMetadata{}, // data source info &Provider{}, // vulnerability related search tables &VulnerabilityHandle{}, &VulnerabilityAlias{}, // package related search tables &AffectedPackageHandle{}, // join on package, operating system &UnaffectedPackageHandle{}, // join on package, operating system &OperatingSystem{}, &OperatingSystemSpecifierOverride{}, &Package{}, &PackageSpecifierOverride{}, // CPE related search tables &AffectedCPEHandle{}, // join on CPE &UnaffectedCPEHandle{}, // join on CPE &Cpe{}, // decorations to vulnerability records &KnownExploitedVulnerabilityHandle{}, &EpssHandle{}, &EpssMetadata{}, &CWEHandle{}, } } type ID int64 // core data store ////////////////////////////////////////////////////// type Blob struct { ID ID `gorm:"column:id;primaryKey"` Value string `gorm:"column:value;not null"` } func (b Blob) computeDigest() string { h := xxhash.New64() if _, err := h.Write([]byte(b.Value)); err != nil { log.Errorf("unable to hash blob: %v", err) panic(err) } return fmt.Sprintf("xxh64:%x", h.Sum(nil)) } // non-domain info ////////////////////////////////////////////////////// type DBMetadata struct { BuildTimestamp *time.Time `gorm:"column:build_timestamp;not null"` Model int `gorm:"column:model;not null"` Revision int `gorm:"column:revision;not null"` Addition int `gorm:"column:addition;not null"` } func newSchemaVerFromDBMetadata(m DBMetadata) schemaver.SchemaVer { return schemaver.New(m.Model, m.Revision, m.Addition) } // data source info ////////////////////////////////////////////////////// // Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider // should be scoped to a specific vulnerability dataset, for instance, the "ubuntu" provider for all records from // Canonicals' Ubuntu Security Notices (for all Ubuntu distro versions). type Provider struct { // ID of the Vunnel provider (or sub processor responsible for data records from a single specific source, e.g. "ubuntu") ID string `gorm:"column:id;primaryKey"` // Version of the Vunnel provider (or sub processor equivalent) Version string `gorm:"column:version"` // Processor is the name of the application that processed the data (e.g. "vunnel") Processor string `gorm:"column:processor"` // DateCaptured is the timestamp which the upstream data was pulled and processed DateCaptured *time.Time `gorm:"column:date_captured"` // InputDigest is a self describing hash (e.g. sha256:123... not 123...) of all data used by the provider to generate the vulnerability records InputDigest string `gorm:"column:input_digest"` } func (p *Provider) String() string { if p == nil { return "" } date := "?" if p.DateCaptured != nil { date = p.DateCaptured.UTC().Format(time.RFC3339) } return fmt.Sprintf("%s@v%s from %s using %q at %s", p.ID, p.Version, p.Processor, p.InputDigest, date) } func (p *Provider) cacheKey() string { return strings.ToLower(p.String()) } func (p *Provider) tableName() string { return cpesTableCacheKey } func (p *Provider) rowID() string { return p.ID } func (p *Provider) setRowID(i string) { p.ID = i } func (p *Provider) BeforeCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { if existingID, ok := cacheInst.getString(p); ok { p.setRowID(existingID) } return nil } return fmt.Errorf("provider creation is not supported") } func (p *Provider) AfterCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { cacheInst.set(p) } return nil } // vulnerability related search tables ////////////////////////////////////////////////////// // VulnerabilityHandle represents the pointer to the core advisory record for a single known vulnerability from a specific provider. // indexes: idx_vuln_provider_id: this is used --by-cve to find all vulnerabilities from the NVD provider type VulnerabilityHandle struct { ID ID `gorm:"column:id;primaryKey"` // Name is the unique name for the vulnerability (same as the decoded VulnerabilityBlob.ID) Name string `gorm:"column:name;not null;index,collate:NOCASE;index:idx_vuln_provider_id,collate:NOCASE"` // Status conveys the actionability of the current record (one of "active", "analyzing", "rejected", "disputed") Status VulnerabilityStatus `gorm:"column:status;not null;index,collate:NOCASE"` // PublishedDate is the date the vulnerability record was first published PublishedDate *time.Time `gorm:"column:published_date;index"` // ModifiedDate is the date the vulnerability record was last modified ModifiedDate *time.Time `gorm:"column:modified_date;index"` // WithdrawnDate is the date the vulnerability record was withdrawn WithdrawnDate *time.Time `gorm:"column:withdrawn_date;index"` ProviderID string `gorm:"column:provider_id;not null;index;index:idx_vuln_provider_id,collate:NOCASE"` Provider *Provider `gorm:"foreignKey:ProviderID"` BlobID ID `gorm:"column:blob_id;index,unique"` BlobValue *VulnerabilityBlob `gorm:"-"` } func (v VulnerabilityHandle) String() string { return fmt.Sprintf("%s/%s", v.Provider, v.Name) } func (v VulnerabilityHandle) getBlobValue() any { if v.BlobValue == nil { return nil // must return untyped nil or getBlobValue() == nil will always be false } return v.BlobValue } func (v *VulnerabilityHandle) setBlobID(id ID) { v.BlobID = id } func (v VulnerabilityHandle) getBlobID() ID { return v.BlobID } func (v *VulnerabilityHandle) setBlob(rawBlobValue []byte) error { var blobValue VulnerabilityBlob if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil { return fmt.Errorf("unable to unmarshal vulnerability blob value: %w", err) } v.BlobValue = &blobValue return nil } func (v *VulnerabilityHandle) cacheKey() string { provider := "none" if v.Provider != nil { provider = v.Provider.ID } return strings.ToLower(fmt.Sprintf("%s from %s with %d", v.Name, provider, v.BlobID)) } func (v *VulnerabilityHandle) rowID() ID { return v.ID } func (v *VulnerabilityHandle) tableName() string { return vulnerabilitiesTableCacheKey } func (v *VulnerabilityHandle) setRowID(i ID) { v.ID = i } func (v *VulnerabilityHandle) BeforeCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { if existing, ok := cacheInst.getID(v); ok { v.setRowID(existing) } return nil } return fmt.Errorf("vulnerability creation is not supported") } func (v *VulnerabilityHandle) AfterCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { cacheInst.set(v) } return nil } type VulnerabilityAlias struct { // Name is the unique name for the vulnerability Name string `gorm:"column:name;primaryKey;index,collate:NOCASE"` // Alias is an alternative name for the vulnerability that must be upstream from the Name (e.g if name is "RHSA-1234" then the upstream could be "CVE-1234-5678", but not the other way around) Alias string `gorm:"column:alias;primaryKey;index,collate:NOCASE;not null"` } // package related search tables ////////////////////////////////////////////////////// // packageHandle represents a single package affected or unaffected by the specified vulnerability. // This is a shared struct used by both AffectedPackageHandle and UnaffectedPackageHandle. This is not a table itself. type packageHandle struct { ID ID `gorm:"column:id;primaryKey"` VulnerabilityID ID `gorm:"column:vulnerability_id;index;not null"` Vulnerability *VulnerabilityHandle `gorm:"foreignKey:VulnerabilityID"` OperatingSystemID *ID `gorm:"column:operating_system_id;index"` OperatingSystem *OperatingSystem `gorm:"foreignKey:OperatingSystemID"` PackageID ID `gorm:"column:package_id;index"` Package *Package `gorm:"foreignKey:PackageID"` BlobID ID `gorm:"column:blob_id"` BlobValue *PackageBlob `gorm:"-"` } func (ph packageHandle) vulnerability() string { if ph.Vulnerability != nil { return ph.Vulnerability.Name } if ph.BlobValue != nil { if len(ph.BlobValue.CVEs) > 0 { return ph.BlobValue.CVEs[0] } } return "" } func (ph packageHandle) String() string { var fields []string if ph.BlobValue != nil { v := ph.BlobValue.String() if v != "" { fields = append(fields, v) } } if ph.OperatingSystem != nil { fields = append(fields, fmt.Sprintf("os=%q", ph.OperatingSystem.String())) } else { fields = append(fields, fmt.Sprintf("os=%d", ph.OperatingSystemID)) } if ph.Package != nil { fields = append(fields, fmt.Sprintf("pkg=%q", ph.Package.String())) } else { fields = append(fields, fmt.Sprintf("pkg=%d", ph.PackageID)) } if ph.Vulnerability != nil { fields = append(fields, fmt.Sprintf("vuln=%q", ph.Vulnerability.String())) } else { fields = append(fields, fmt.Sprintf("vuln=%d", ph.VulnerabilityID)) } return fmt.Sprintf("package(%s)", strings.Join(fields, ", ")) } func (ph packageHandle) getBlobValue() any { if ph.BlobValue == nil { return nil // must return untyped nil or getBlobValue() == nil will always be false } return ph.BlobValue } func (ph *packageHandle) setBlobID(id ID) { ph.BlobID = id } func (ph packageHandle) getBlobID() ID { return ph.BlobID } func (ph *packageHandle) setBlob(rawBlobValue []byte) error { var blobValue PackageBlob if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil { return fmt.Errorf("unable to unmarshal affected package blob value: %w", err) } ph.BlobValue = &blobValue return nil } // AffectedPackageHandle represents a single package affected by the specified vulnerability. // // A package here is a name within a known ecosystem, such as "python" or "golang". It is important to note that this // table relates vulnerabilities to resolved packages. There are cases when we have package identifiers but are not // resolved to packages; for example, when we have a CPE but not a clear understanding of the package ecosystem and // authoritative name (which might or might not be the product name in the CPE), in which case AffectedCPEHandle // should be used. type AffectedPackageHandle packageHandle func (ph *AffectedPackageHandle) getPackageHandle() *packageHandle { return (*packageHandle)(ph) } func (ph AffectedPackageHandle) vulnerability() string { // nolint:unused // when implementing filter functions in the future this will be needed return (packageHandle)(ph).vulnerability() } func (ph AffectedPackageHandle) String() string { return (packageHandle)(ph).String() } func (ph AffectedPackageHandle) getBlobValue() any { return (packageHandle)(ph).getBlobValue() } func (ph *AffectedPackageHandle) setBlobID(id ID) { (*packageHandle)(ph).setBlobID(id) } func (ph AffectedPackageHandle) getBlobID() ID { return (packageHandle)(ph).getBlobID() } func (ph *AffectedPackageHandle) setBlob(rawBlobValue []byte) error { return (*packageHandle)(ph).setBlob(rawBlobValue) } // UnaffectedPackageHandle represents a single package that is explicitly NOT affected by the specified vulnerability. type UnaffectedPackageHandle packageHandle func (ph *UnaffectedPackageHandle) getPackageHandle() *packageHandle { return (*packageHandle)(ph) } func (ph UnaffectedPackageHandle) vulnerability() string { // nolint:unused // when implementing filter functions in the future this will be needed return (packageHandle)(ph).vulnerability() } func (ph UnaffectedPackageHandle) String() string { return (packageHandle)(ph).String() } func (ph UnaffectedPackageHandle) getBlobValue() any { return (packageHandle)(ph).getBlobValue() } func (ph *UnaffectedPackageHandle) setBlobID(id ID) { (*packageHandle)(ph).setBlobID(id) } func (ph UnaffectedPackageHandle) getBlobID() ID { return (packageHandle)(ph).getBlobID() } func (ph *UnaffectedPackageHandle) setBlob(rawBlobValue []byte) error { return (*packageHandle)(ph).setBlob(rawBlobValue) } // Package represents a package name within a known ecosystem, such as "python" or "golang". type Package struct { ID ID `gorm:"column:id;primaryKey"` // Ecosystem is the tooling and language ecosystem that the package is released within Ecosystem string `gorm:"column:ecosystem;index:idx_package,unique,collate:NOCASE"` // Name is the name of the package within the ecosystem Name string `gorm:"column:name;index:idx_package,unique,collate:NOCASE;index:idx_package_name,collate:NOCASE"` // CPEs is the list of Common Platform Enumeration (CPE) identifiers that represent this package CPEs []Cpe `gorm:"many2many:package_cpes;"` } func (p Package) String() string { var cpes []string for _, cpe := range p.CPEs { cpes = append(cpes, cpe.String()) } if p.Ecosystem != "" && p.Name != "" { base := fmt.Sprintf("%s/%s", p.Ecosystem, p.Name) if len(cpes) == 0 { return base } return fmt.Sprintf("%s (%s)", base, strings.Join(cpes, ", ")) } return strings.Join(cpes, ", ") } func (p Package) cacheKey() string { if p.Ecosystem == "" && p.Name == "" { return "" } // we're intentionally not including anything about CPEs here, since there is potentially a merge operation for // packages with CPEs we cannot reason about packages with CPEs in the cache, they must always pass through. return strings.ToLower(fmt.Sprintf("%s/%s", p.Ecosystem, p.Name)) } func (p Package) rowID() ID { return p.ID } func (p *Package) tableName() string { return packagesTableCacheKey } func (p *Package) setRowID(i ID) { p.ID = i } func (p *Package) BeforeCreate(tx *gorm.DB) (err error) { // nolint:gocognit cacheInst, ok := cacheFromContext(tx.Statement.Context) if !ok { return fmt.Errorf("cache not found in context") } var existingPackage Package err = tx.Preload("CPEs").Where("ecosystem = ? collate nocase AND name = ? collate nocase", p.Ecosystem, p.Name).First(&existingPackage).Error if err == nil { // package exists; merge CPEs for _, newCPE := range p.CPEs { var existingCPE Cpe if existingID, ok := cacheInst.getID(&newCPE); ok { if err := tx.Where("id = ?", existingID).First(&existingCPE).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("failed to find CPE by ID %d: %w", existingID, err) } } } if existingCPE.ID != 0 { // if the record already exists, then we should use the existing record continue } // if the CPE does not exist, proceed with creating it existingPackage.CPEs = append(existingPackage.CPEs, newCPE) if err := tx.Create(&newCPE).Error; err != nil { return fmt.Errorf("failed to create CPE %v for package %v: %w", newCPE, existingPackage, err) } } // use the existing package instead of creating a new one *p = existingPackage return nil } return nil } func (p *Package) AfterCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { cacheInst.set(p) for _, cpe := range p.CPEs { cacheInst.set(&cpe) } } return nil } // PackageSpecifierOverride is a table that allows for overriding fields on v6.PackageSpecifier instances when searching for specific Packages. type PackageSpecifierOverride struct { Ecosystem string `gorm:"column:ecosystem;primaryKey;index:pkg_ecosystem_idx,collate:NOCASE"` // below are the fields that should be used as replacement for fields in the Packages table ReplacementEcosystem *string `gorm:"column:replacement_ecosystem;primaryKey"` } // OperatingSystem represents a specific release of an operating system. The resolution of the version is // relative to the available data by the vulnerability data provider, so though there may be major.minor.patch OS // releases, there may only be data available for major.minor. type OperatingSystem struct { ID ID `gorm:"column:id;primaryKey"` // Name is the operating system family name (e.g. "debian") Name string `gorm:"column:name;index:os_idx,unique;index,collate:NOCASE"` ReleaseID string `gorm:"column:release_id;index:os_idx,unique;index,collate:NOCASE"` // MajorVersion is the major version of a specific release (e.g. "10" for debian 10) MajorVersion string `gorm:"column:major_version;index:os_idx,unique;index"` // MinorVersion is the minor version of a specific release (e.g. "1" for debian 10.1) MinorVersion string `gorm:"column:minor_version;index:os_idx,unique;index"` // LabelVersion is an optional non-codename string representation of the version (e.g. "unstable" or for debian:sid) LabelVersion string `gorm:"column:label_version;index:os_idx,unique;index,collate:NOCASE"` // Codename is the codename of a specific release (e.g. "buster" for debian 10) Codename string `gorm:"column:codename;index,collate:NOCASE"` // Channel is a string used to distinguish between fix and vulnerability data for the same OS release. // such as RHEL-9.4+EUS vs RHEL-9 Channel string `gorm:"column:channel;index:os_idx,unique;index,collate:NOCASE"` // EOLDate is when this OS release reaches end-of-life (no more security updates) EOLDate *time.Time `gorm:"column:eol_date;index"` // EOASDate is when this OS release reaches end-of-active-support (reduced support, before full EOL) EOASDate *time.Time `gorm:"column:eoas_date"` } func (o *OperatingSystem) VersionNumber() string { if o == nil { return "" } if o.MinorVersion != "" { return fmt.Sprintf("%s.%s", o.MajorVersion, o.MinorVersion) } return o.MajorVersion } func (o *OperatingSystem) Version() string { if o == nil { return "" } if o.LabelVersion != "" { return o.LabelVersion } var suffix string if o.Channel != "" { suffix = fmt.Sprintf("+%s", o.Channel) } if o.MajorVersion != "" { if o.MinorVersion != "" { return fmt.Sprintf("%s.%s%s", o.MajorVersion, o.MinorVersion, suffix) } return o.MajorVersion + suffix } return o.Codename } func (o OperatingSystem) String() string { return fmt.Sprintf("%s@%s", o.Name, o.Version()) } func (o OperatingSystem) cacheKey() string { return strings.ToLower(o.String()) } func (o OperatingSystem) rowID() ID { return o.ID } func (o *OperatingSystem) tableName() string { return operatingSystemsTableCacheKey } func (o *OperatingSystem) setRowID(i ID) { o.ID = i } func (o *OperatingSystem) clean() { o.MajorVersion = trimZeroes(o.MajorVersion) o.MinorVersion = trimZeroes(o.MinorVersion) } func (o *OperatingSystem) BeforeCreate(tx *gorm.DB) (err error) { o.clean() if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { if existing, ok := cacheInst.getID(o); ok { o.setRowID(existing) } return nil } return fmt.Errorf("OS creation is not supported") } func (o *OperatingSystem) AfterCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { cacheInst.set(o) } return nil } // OperatingSystemSpecifierOverride is a table that allows for overriding fields on v6.OSSpecifier instances when searching for specific OperatingSystems. type OperatingSystemSpecifierOverride struct { // Alias is an alternative name/ID for the operating system. Alias string `gorm:"column:alias;primaryKey;index:os_alias_idx,collate:NOCASE"` // Version is the matching version as found in the VERSION_ID field if the /etc/os-release file Version string `gorm:"column:version;primaryKey"` // VersionPattern is a regex pattern to match against the VERSION_ID field if the /etc/os-release file VersionPattern string `gorm:"column:version_pattern;primaryKey"` // Codename is the matching codename as found in the VERSION_CODENAME field if the /etc/os-release file Codename string `gorm:"column:codename;collate:NOCASE"` // Channel is a string used to distinguish between fix and vulnerability data for the same OS release (e.g. RHEL mainline vs EUS). Channel string `gorm:"column:channel;collate:NOCASE"` // below are the fields that should be used as replacement for fields in the OperatingSystem table ReplacementName *string `gorm:"column:replacement;primaryKey"` ReplacementMajorVersion *string `gorm:"column:replacement_major_version;primaryKey"` ReplacementMinorVersion *string `gorm:"column:replacement_minor_version;primaryKey"` ReplacementLabelVersion *string `gorm:"column:replacement_label_version;primaryKey"` ReplacementChannel *string `gorm:"column:replacement_channel;primaryKey"` Rolling bool `gorm:"column:rolling;primaryKey"` // ApplicableClientDBSchemas is a constraint on the database version that this override can be applied to (relative to the client library being used to access the DB). ApplicableClientDBSchemas string `gorm:"column:applicable_client_db_schemas"` } func (os *OperatingSystemSpecifierOverride) BeforeCreate(_ *gorm.DB) (err error) { if os.Version != "" && os.VersionPattern != "" { return fmt.Errorf("cannot have both version and version_pattern set") } return nil } // CPE related search tables ////////////////////////////////////////////////////// // AffectedCPEHandle represents a single CPE affected by the specified vulnerability. // This is a shared struct used by both AffectedCPEHandle and UnaffectedCPEHandle. This is not a table itself. type cpeHandle struct { ID ID `gorm:"column:id;primaryKey"` VulnerabilityID ID `gorm:"column:vulnerability_id;not null"` Vulnerability *VulnerabilityHandle `gorm:"foreignKey:VulnerabilityID"` CpeID ID `gorm:"column:cpe_id;index"` CPE *Cpe `gorm:"foreignKey:CpeID"` BlobID ID `gorm:"column:blob_id"` BlobValue *PackageBlob `gorm:"-"` } func (ch cpeHandle) vulnerability() string { if ch.Vulnerability != nil { return ch.Vulnerability.Name } if ch.BlobValue != nil { if len(ch.BlobValue.CVEs) > 0 { return ch.BlobValue.CVEs[0] } } return "" } func (ch cpeHandle) String() string { var fields []string if ch.BlobValue != nil { v := ch.BlobValue.String() if v != "" { fields = append(fields, v) } } if ch.CPE != nil { fields = append(fields, fmt.Sprintf("cpe=%q", ch.CPE.String())) } else { fields = append(fields, fmt.Sprintf("cpe=%d", ch.CpeID)) } if ch.Vulnerability != nil { fields = append(fields, fmt.Sprintf("vuln=%q", ch.Vulnerability.String())) } else { fields = append(fields, fmt.Sprintf("vuln=%d", ch.VulnerabilityID)) } return fmt.Sprintf("cpe(%s)", strings.Join(fields, ", ")) } func (ch cpeHandle) getBlobID() ID { return ch.BlobID } func (ch cpeHandle) getBlobValue() any { if ch.BlobValue == nil { return nil // must return untyped nil or getBlobValue() == nil will always be false } return ch.BlobValue } func (ch *cpeHandle) setBlobID(id ID) { ch.BlobID = id } func (ch *cpeHandle) setBlob(rawBlobValue []byte) error { var blobValue PackageBlob if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil { return fmt.Errorf("unable to unmarshal affected cpe blob value: %w", err) } ch.BlobValue = &blobValue return nil } // AffectedCPEHandle represents a single CPE affected by the specified vulnerability. // // Note the CPEs in this table must NOT be resolvable to Packages (use AffectedPackageHandle for that). This table is // used when the CPE is known, but we do not have a clear understanding of the package ecosystem or authoritative // name, so we can still find vulnerabilities by these identifiers but not assert they are related to an entry in // the AffectedPackages table. type AffectedCPEHandle cpeHandle func (ch *AffectedCPEHandle) getCPEHandle() *cpeHandle { return (*cpeHandle)(ch) } func (ch AffectedCPEHandle) vulnerability() string { return (cpeHandle)(ch).vulnerability() } func (ch AffectedCPEHandle) String() string { return (cpeHandle)(ch).String() } func (ch AffectedCPEHandle) getBlobID() ID { return (cpeHandle)(ch).getBlobID() } func (ch AffectedCPEHandle) getBlobValue() any { return (cpeHandle)(ch).getBlobValue() } func (ch *AffectedCPEHandle) setBlobID(id ID) { (*cpeHandle)(ch).setBlobID(id) } func (ch *AffectedCPEHandle) setBlob(rawBlobValue []byte) error { return (*cpeHandle)(ch).setBlob(rawBlobValue) } type UnaffectedCPEHandle cpeHandle func (ch *UnaffectedCPEHandle) getCPEHandle() *cpeHandle { return (*cpeHandle)(ch) } func (ch UnaffectedCPEHandle) vulnerability() string { // nolint:unused // when implementing filter functions in the future this will be needed return (cpeHandle)(ch).vulnerability() } func (ch UnaffectedCPEHandle) String() string { return (cpeHandle)(ch).String() } func (ch UnaffectedCPEHandle) getBlobID() ID { return (cpeHandle)(ch).getBlobID() } func (ch UnaffectedCPEHandle) getBlobValue() any { return (cpeHandle)(ch).getBlobValue() } func (ch *UnaffectedCPEHandle) setBlobID(id ID) { (*cpeHandle)(ch).setBlobID(id) } func (ch *UnaffectedCPEHandle) setBlob(rawBlobValue []byte) error { return (*cpeHandle)(ch).setBlob(rawBlobValue) } type Cpe struct { // TODO: what about different CPE versions? ID ID `gorm:"primaryKey"` Part string `gorm:"column:part;not null;index:idx_cpe,unique,collate:NOCASE"` Vendor string `gorm:"column:vendor;index:idx_cpe,unique,collate:NOCASE;index:idx_cpe_vendor,collate:NOCASE"` Product string `gorm:"column:product;not null;index:idx_cpe,unique,collate:NOCASE;index:idx_cpe_product,collate:NOCASE"` Edition string `gorm:"column:edition;index:idx_cpe,unique,collate:NOCASE"` Language string `gorm:"column:language;index:idx_cpe,unique,collate:NOCASE"` SoftwareEdition string `gorm:"column:software_edition;index:idx_cpe,unique,collate:NOCASE"` TargetHardware string `gorm:"column:target_hardware;index:idx_cpe,unique,collate:NOCASE"` TargetSoftware string `gorm:"column:target_software;index:idx_cpe,unique,collate:NOCASE"` Other string `gorm:"column:other;index:idx_cpe,unique,collate:NOCASE"` Packages []Package `gorm:"many2many:package_cpes;"` } func (c Cpe) String() string { parts := []string{"cpe:2.3", c.Part, c.Vendor, c.Product, "*", "*", c.Edition, c.Language, c.SoftwareEdition, c.TargetSoftware, c.TargetHardware, c.Other} for i, part := range parts { if part == "" { parts[i] = "*" } } return strings.Join(parts, ":") } func (c *Cpe) cacheKey() string { return strings.ToLower(c.String()) } func (c *Cpe) tableName() string { return cpesTableCacheKey } func (c *Cpe) rowID() ID { return c.ID } func (c *Cpe) setRowID(i ID) { c.ID = i } func (c *Cpe) BeforeCreate(tx *gorm.DB) (err error) { cacheInst, ok := cacheFromContext(tx.Statement.Context) if !ok { return fmt.Errorf("CPE creation is not supported") } if existingID, ok := cacheInst.getID(c); ok { var existing Cpe result := tx.Where("id = ?", existingID).First(&existing) if result.Error == nil { // if the record already exists, then we should use the existing record *c = existing } c.setRowID(existingID) } return nil } func (c *Cpe) AfterCreate(tx *gorm.DB) (err error) { if cacheInst, ok := cacheFromContext(tx.Statement.Context); ok { cacheInst.set(c) } return nil } // PackageCpe join table for the many-to-many relationship type PackageCpe struct { PackageID ID `gorm:"primaryKey;column:package_id"` CpeID ID `gorm:"primaryKey;column:cpe_id"` } func (PackageCpe) TableName() string { // note: this value is referenced in multiple struct tags and must not be changed or removed // without this override the table name would be both model names in alphabetical order: cpes_packages return "package_cpes" } type KnownExploitedVulnerabilityHandle struct { ID int64 `gorm:"primaryKey"` Cve string `gorm:"column:cve;not null;index:kev_cve_idx,collate:NOCASE"` BlobID ID `gorm:"column:blob_id"` BlobValue *KnownExploitedVulnerabilityBlob `gorm:"-"` } func (v KnownExploitedVulnerabilityHandle) getBlobValue() any { if v.BlobValue == nil { return nil // must return untyped nil or getBlobValue() == nil will always be false } return v.BlobValue } func (v *KnownExploitedVulnerabilityHandle) setBlobID(id ID) { v.BlobID = id } func (v KnownExploitedVulnerabilityHandle) getBlobID() ID { return v.BlobID } func (v *KnownExploitedVulnerabilityHandle) setBlob(rawBlobValue []byte) error { var blobValue KnownExploitedVulnerabilityBlob if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil { return fmt.Errorf("unable to unmarshal KEV blob value: %w", err) } v.BlobValue = &blobValue return nil } type EpssMetadata struct { Date time.Time `gorm:"column:date;not null"` } type EpssHandle struct { ID int64 `gorm:"primaryKey"` Cve string `gorm:"column:cve;not null;index:epss_cve_idx,collate:NOCASE"` Epss float64 `gorm:"column:epss;not null"` Percentile float64 `gorm:"column:percentile;not null"` Date time.Time `gorm:"-"` // note we do not store the date in this table since it is expected to be the same for all records, that is what EpssMetadata is for } type CWEHandle struct { ID int64 `gorm:"primaryKey"` CVE string `gorm:"column:cve;not null;index:cwes_cve_idx,collate:NOCASE"` CWE string `gorm:"column:cwe;not null;"` Source string `gorm:"column:source;"` Type string `gorm:"column:type;"` } func (c CWEHandle) String() string { return fmt.Sprintf("CWE(%s: %s, source=%s, type=%s)", c.CVE, c.CWE, c.Source, c.Type) } // OperatingSystemEOLHandle carries end-of-life data for an operating system. // This is not a GORM model - it's used to update existing OperatingSystem records. type OperatingSystemEOLHandle struct { Name string // distro name (e.g., "debian", "ubuntu") MajorVersion string // major version (e.g., "12") MinorVersion string // minor version (e.g., "04" for ubuntu) Codename string // optional codename EOLDate *time.Time // end-of-life date EOASDate *time.Time // end-of-active-support date } func (o OperatingSystemEOLHandle) String() string { eol := "nil" if o.EOLDate != nil { eol = o.EOLDate.Format("2006-01-02") } return fmt.Sprintf("OSEol(%s %s.%s, eol=%s)", o.Name, o.MajorVersion, o.MinorVersion, eol) } ================================================ FILE: grype/db/v6/models_test.go ================================================ package v6 import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOperatingSystemAlias_VersionMutualExclusivity(t *testing.T) { db := setupTestStore(t).db msg := "cannot have both version and version_pattern set" tests := []struct { name string input *OperatingSystemSpecifierOverride errMsg string }{ { name: "version and version_pattern are mutually exclusive", input: &OperatingSystemSpecifierOverride{ Alias: "ubuntu", Version: "20.04", VersionPattern: "20.*", }, errMsg: msg, }, { name: "only version is set", input: &OperatingSystemSpecifierOverride{ Alias: "ubuntu", Version: "20.04", }, errMsg: "", }, { name: "only version_pattern is set", input: &OperatingSystemSpecifierOverride{ Alias: "ubuntu", VersionPattern: "20.*", }, errMsg: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := db.Create(tt.input).Error if tt.errMsg == "" { assert.NoError(t, err) } else { require.Error(t, err) assert.Contains(t, err.Error(), tt.errMsg) } }) } } func TestOperatingSystem_VersionNumber(t *testing.T) { tests := []struct { name string os *OperatingSystem expectedResult string }{ { name: "nil OS", os: nil, expectedResult: "", }, { name: "major and minor versions", os: &OperatingSystem{MajorVersion: "10", MinorVersion: "1"}, expectedResult: "10.1", }, { name: "major version only", os: &OperatingSystem{MajorVersion: "10"}, expectedResult: "10", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expectedResult, tt.os.VersionNumber()) }) } } func TestOperatingSystem_Version(t *testing.T) { tests := []struct { name string os *OperatingSystem expectedResult string }{ { name: "nil OS", os: nil, expectedResult: "", }, { name: "label version", os: &OperatingSystem{LabelVersion: "unstable"}, expectedResult: "unstable", }, { name: "major and minor versions", os: &OperatingSystem{MajorVersion: "10", MinorVersion: "1"}, expectedResult: "10.1", }, { name: "major version only", os: &OperatingSystem{MajorVersion: "10"}, expectedResult: "10", }, { name: "codename", os: &OperatingSystem{Codename: "buster"}, expectedResult: "buster", }, { name: "with channel", os: &OperatingSystem{MajorVersion: "10", MinorVersion: "1", Channel: "stable"}, expectedResult: "10.1+stable", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expectedResult, tt.os.Version()) }) } } func TestOperatingSystem_clean(t *testing.T) { tests := []struct { name string input OperatingSystem want OperatingSystem }{ { name: "trim 0s", input: OperatingSystem{ Name: "Ubuntu", MajorVersion: "20", MinorVersion: "04", }, want: OperatingSystem{ Name: "Ubuntu", MajorVersion: "20", MinorVersion: "4", }, }, { name: "preserve 0 value", input: OperatingSystem{ Name: "Redhat", MajorVersion: "9", MinorVersion: "0", }, want: OperatingSystem{ Name: "Redhat", MajorVersion: "9", MinorVersion: "0", // important! ...9 != 9.0 since 9 includes multiple minor versions }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := tt.input o.clean() if d := cmp.Diff(tt.want, o); d != "" { t.Errorf("OperatingSystem.clean() mismatch (-want +got):\n%s", d) } }) } } ================================================ FILE: grype/db/v6/name/java.go ================================================ package name import ( "fmt" grypePkg "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" ) type JavaResolver struct { } func (r *JavaResolver) Normalize(name string) string { return name } func (r *JavaResolver) Names(p grypePkg.Package) []string { names := stringutil.NewStringSet() // The current default for the Java ecosystem is to use a Maven-like identifier of the form // ":" if metadata, ok := p.Metadata.(grypePkg.JavaMetadata); ok { if metadata.PomGroupID != "" { if metadata.PomArtifactID != "" { names.Add(r.Normalize(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.PomArtifactID))) } if metadata.ManifestName != "" { names.Add(r.Normalize(fmt.Sprintf("%s:%s", metadata.PomGroupID, metadata.ManifestName))) } } } if p.PURL != "" { purl, err := packageurl.FromString(p.PURL) if err != nil { log.Warnf("unable to resolve java package identifier from purl=%q: %+v", p.PURL, err) } else { names.Add(r.Normalize(fmt.Sprintf("%s:%s", purl.Namespace, purl.Name))) } } return names.ToSlice() } ================================================ FILE: grype/db/v6/name/java_test.go ================================================ package name import ( "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" grypePkg "github.com/anchore/grype/grype/pkg" ) func TestJavaResolver_Normalize(t *testing.T) { tests := []struct { name string normalized string }{ { name: "PyYAML", // note we are not lowercasing since the DB is case-insensitive for name columns normalized: "PyYAML", }, { name: "oslo.concurrency", normalized: "oslo.concurrency", }, { name: "", normalized: "", }, { name: "test---1", normalized: "test---1", }, { name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ", normalized: "AbCd.-__.--.-___.__.--1234____----....XyZZZ", }, } resolver := JavaResolver{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolvedNames := resolver.Normalize(test.name) assert.Equal(t, resolvedNames, test.normalized) }) } } func TestJavaResolver_Names(t *testing.T) { tests := []struct { name string pkg grypePkg.Package resolved []string }{ { name: "both artifact and manifest 1", pkg: grypePkg.Package{ Name: "ABCD", Version: "1.2.3.4", Language: "java", Metadata: grypePkg.JavaMetadata{ VirtualPath: "virtual-path-info", PomArtifactID: "pom-ARTIFACT-ID-info", PomGroupID: "pom-group-ID-info", ManifestName: "main-section-name-info", }, }, resolved: []string{"pom-group-ID-info:pom-ARTIFACT-ID-info", "pom-group-ID-info:main-section-name-info"}, }, { name: "both artifact and manifest 2", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", PomGroupID: "g-id", ManifestName: "man-name", }, }, resolved: []string{ "g-id:art-id", "g-id:man-name", }, }, { name: "no group id", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", ManifestName: "man-name", }, }, resolved: []string{}, }, { name: "only manifest", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomGroupID: "g-id", ManifestName: "man-name", }, }, resolved: []string{ "g-id:man-name", }, }, { name: "only artifact", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", PomGroupID: "g-id", }, }, resolved: []string{ "g-id:art-id", }, }, { name: "no artifact or manifest", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomGroupID: "g-id", }, }, resolved: []string{}, }, { name: "with valid purl", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", PURL: "pkg:maven/org.anchore/b-name@0.2", }, resolved: []string{"org.anchore:b-name"}, }, { name: "ignore invalid pURLs", pkg: grypePkg.Package{ ID: grypePkg.ID(uuid.NewString()), Name: "a-name", PURL: "pkg:BAD/", Metadata: grypePkg.JavaMetadata{ VirtualPath: "v-path", PomArtifactID: "art-id", PomGroupID: "g-id", }, }, resolved: []string{ "g-id:art-id", }, }, } resolver := JavaResolver{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolvedNames := resolver.Names(test.pkg) assert.ElementsMatch(t, resolvedNames, test.resolved) }) } } ================================================ FILE: grype/db/v6/name/python.go ================================================ package name import ( "regexp" grypePkg "github.com/anchore/grype/grype/pkg" ) type PythonResolver struct { } func (r *PythonResolver) Normalize(name string) string { // Canonical naming of packages within python is defined by PEP 503 at // https://peps.python.org/pep-0503/#normalized-names, and this code is derived from // the official python implementation of canonical naming at // https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name return regexp.MustCompile(`[-_.]+`).ReplaceAllString(name, "-") } func (r *PythonResolver) Names(p grypePkg.Package) []string { // Canonical naming of packages within python is defined by PEP 503 at // https://peps.python.org/pep-0503/#normalized-names, and this code is derived from // the official python implementation of canonical naming at // https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name return []string{r.Normalize(p.Name)} } ================================================ FILE: grype/db/v6/name/python_test.go ================================================ package name import ( "testing" "github.com/stretchr/testify/assert" ) func TestPythonResolver_Normalize(t *testing.T) { tests := []struct { name string normalized string }{ { name: "PyYAML", // note we are not lowercasing since the DB is case-insensitive for name columns normalized: "PyYAML", }, { name: "oslo.concurrency", normalized: "oslo-concurrency", }, { name: "", normalized: "", }, { name: "test---1", normalized: "test-1", }, { name: "AbCd.-__.--.-___.__.--1234____----....XyZZZ", normalized: "AbCd-1234-XyZZZ", }, } resolver := PythonResolver{} for _, test := range tests { t.Run(test.name, func(t *testing.T) { resolvedNames := resolver.Normalize(test.name) assert.Equal(t, resolvedNames, test.normalized) }) } } ================================================ FILE: grype/db/v6/name/resolver.go ================================================ package name import ( grypePkg "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" ) type Resolver interface { Normalize(string) string Names(p grypePkg.Package) []string } func FromType(t syftPkg.Type) Resolver { switch t { case syftPkg.PythonPkg: return &PythonResolver{} case syftPkg.JavaPkg, syftPkg.JenkinsPluginPkg: return &JavaResolver{} } return nil } func PackageNames(p grypePkg.Package) []string { names := []string{p.Name} r := FromType(p.Type) if r == nil { return names } parts := r.Names(p) if len(parts) > 0 { names = parts } return names } func Normalize(name string, pkgType syftPkg.Type) string { r := FromType(pkgType) if r != nil { return r.Normalize(name) } return name } ================================================ FILE: grype/db/v6/operating_system_store.go ================================================ package v6 import ( "errors" "fmt" "regexp" "strings" "time" "gorm.io/gorm" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/internal/log" ) type OSSpecifiers []*OSSpecifier // OSSpecifier is a struct that represents a distro in a way that can be used to query the affected package store. type OSSpecifier struct { // Name of the distro as identified by the ID field in /etc/os-release (or similar normalized name, e.g. "oracle" instead of "ol") Name string // MajorVersion is the first field in the VERSION_ID field in /etc/os-release (e.g. 7 in "7.0.1406") MajorVersion string // MinorVersion is the second field in the VERSION_ID field in /etc/os-release (e.g. 0 in "7.0.1406") MinorVersion string // RemainingVersion is anything after the minor version in the VERSION_ID field in /etc/os-release (e.g. 1406 in "7.0.1406") RemainingVersion string // LabelVersion is a string that represents a floating version (e.g. "edge" or "unstable") or is the CODENAME field in /etc/os-release (e.g. "wheezy" for debian 7) LabelVersion string // Channel is a string that represents a different feed for fix and vulnerability data (e.g. "eus" for RHEL) Channel string // DisableAliasing prevents OS aliasing when true (used for exact distro matching) DisableAliasing bool // DisableFallback prevents fallback to less specific version matching when true. // When set, only exact version matches are returned (no major-only fallback). // Used for EOL lookups where we don't want e.g. Alpine 3.24 to match Alpine 3.12. DisableFallback bool } func (d *OSSpecifier) clean() { d.MajorVersion = trimZeroes(d.MajorVersion) d.MinorVersion = trimZeroes(d.MinorVersion) } func (d *OSSpecifier) String() string { if d == nil { return anyOS } if *d == *NoOSSpecified { return "none" } var ver string if d.MajorVersion != "" { ver = d.version() } else { ver = d.LabelVersion } distroDisplayName := d.Name if ver != "" { distroDisplayName += "@" + ver } if ver == d.MajorVersion && d.LabelVersion != "" { distroDisplayName += " (" + d.LabelVersion + ")" } return distroDisplayName } func (d OSSpecifier) version() string { if d.MajorVersion != "" { if d.MinorVersion != "" { if d.RemainingVersion != "" { return d.MajorVersion + "." + d.MinorVersion + "." + d.RemainingVersion } return d.MajorVersion + "." + d.MinorVersion } return d.MajorVersion } return d.LabelVersion } func (d OSSpecifiers) String() string { if d.IsAny() { return anyOS } var parts []string for _, v := range d { parts = append(parts, v.String()) } return strings.Join(parts, ", ") } func (d OSSpecifiers) IsAny() bool { if len(d) == 0 { return true } if len(d) == 1 && d[0] == AnyOSSpecified { return true } return false } func (d OSSpecifier) matchesVersionPattern(pattern string) bool { // check if version or version label matches the given regex r, err := regexp.Compile(pattern) if err != nil { log.Tracef("failed to compile distro specifier regex pattern %q: %v", pattern, err) return false } if r.MatchString(d.version()) { return true } if d.LabelVersion != "" { return r.MatchString(d.LabelVersion) } return false } type OperatingSystemStoreReader interface { GetOperatingSystems(OSSpecifier) ([]OperatingSystem, error) } type OperatingSystemStoreWriter interface { // UpdateOperatingSystemEOL updates the EOL and EOAS dates for an operating system // matching the given specifier. Returns the number of records updated. UpdateOperatingSystemEOL(spec OSSpecifier, eolDate, eoasDate *time.Time) (int64, error) } type operatingSystemStore struct { db *gorm.DB blobStore *blobStore clientVersion *version.Version } func newOperatingSystemStore(db *gorm.DB, bs *blobStore) *operatingSystemStore { return &operatingSystemStore{ db: db, blobStore: bs, clientVersion: version.New(fmt.Sprintf("%d.%d.%d", ModelVersion, Revision, Addition), version.SemanticFormat), } } func (s *operatingSystemStore) addOsFromPackages(packages ...*packageHandle) error { // nolint:dupl cacheInst, ok := cacheFromContext(s.db.Statement.Context) if !ok { return fmt.Errorf("unable to fetch OS cache from context") } var final []*OperatingSystem byCacheKey := make(map[string][]*OperatingSystem) for _, p := range packages { if p.OperatingSystem != nil { p.OperatingSystem.clean() key := p.OperatingSystem.cacheKey() if existingID, ok := cacheInst.getID(p.OperatingSystem); ok { // seen in a previous transaction... p.OperatingSystemID = &existingID } else if _, ok := byCacheKey[key]; !ok { // not seen within this transaction final = append(final, p.OperatingSystem) } byCacheKey[key] = append(byCacheKey[key], p.OperatingSystem) } } if len(final) == 0 { return nil } if err := s.db.Create(final).Error; err != nil { return fmt.Errorf("unable to create OS records: %w", err) } // update the cache with the new records for _, ref := range final { cacheInst.set(ref) } // update all references with the IDs from the cache for _, refs := range byCacheKey { for _, ref := range refs { id, ok := cacheInst.getID(ref) if ok { ref.setRowID(id) } } } // update the parent objects with the FK ID for _, p := range packages { if p.OperatingSystem != nil { p.OperatingSystemID = &p.OperatingSystem.ID } } return nil } func (s *operatingSystemStore) GetOperatingSystems(d OSSpecifier) ([]OperatingSystem, error) { if d.Name == "" && d.LabelVersion == "" { return nil, ErrMissingOSIdentification } // search for aliases for the given distro; we intentionally map some OSs to other OSs in terms of // vulnerability (e.g. `centos` is an alias for `rhel`). If an alias is found always use that alias in // searches (there will never be anything in the DB for aliased distros). // Skip aliasing if explicitly disabled (used for exact distro matching). if !d.DisableAliasing { if err := s.applyOSAlias(&d); err != nil { return nil, err } } d.clean() // handle non-version fields query := s.prepareQuery(d) // handle version-like fields return s.searchForOSExactVersions(query, d) } func (s *operatingSystemStore) applyOSAlias(d *OSSpecifier) error { if d.Name == "" { return nil } var aliases []OperatingSystemSpecifierOverride err := s.db.Where("alias = ? collate nocase", d.Name).Find(&aliases).Error if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("failed to resolve alias for distro %q: %w", d.Name, err) } return nil } return applyOSSpecifierOverrides(d, aliases, s.clientVersion) } func applyOSSpecifierOverrides(d *OSSpecifier, overrides []OperatingSystemSpecifierOverride, clientVersion *version.Version) error { for _, o := range overrides { canUse, err := canUseOverride(o, clientVersion) if err != nil { log.Tracef("failed to check if override %q is applicable for client version %s: %v", o.Alias, clientVersion, err) // we cannot check if we can use this override, so we assume it is applicable } else if !canUse { // this override is not applicable for the current client version continue } if o.Codename != "" && o.Codename != d.LabelVersion { continue } if o.Version != "" && o.Version != d.version() { continue } if o.VersionPattern != "" && !d.matchesVersionPattern(o.VersionPattern) { continue } // first match wins, we do not apply any further overrides applyOverride(d, o) break } return nil } func applyOverride(d *OSSpecifier, override OperatingSystemSpecifierOverride) bool { var applied bool if override.ReplacementName != nil { d.Name = *override.ReplacementName applied = true } if override.Rolling { d.MajorVersion = "" d.MinorVersion = "" applied = true } if override.ReplacementMajorVersion != nil { d.MajorVersion = *override.ReplacementMajorVersion applied = true } if override.ReplacementMinorVersion != nil { d.MinorVersion = *override.ReplacementMinorVersion applied = true } if override.ReplacementLabelVersion != nil { d.LabelVersion = *override.ReplacementLabelVersion applied = true } if override.ReplacementChannel != nil { d.Channel = *override.ReplacementChannel applied = true } return applied } func canUseOverride(override OperatingSystemSpecifierOverride, clientVersion *version.Version) (bool, error) { if override.ApplicableClientDBSchemas == "" || clientVersion == nil { return true, nil } c, err := version.GetConstraint(override.ApplicableClientDBSchemas, version.SemanticFormat) if err != nil { return true, fmt.Errorf("unable to parse version constraint: %w", err) } ok, err := c.Satisfied(clientVersion) if err != nil { return true, fmt.Errorf("unable to check if client constraint: %w", err) } if !ok { // explicitly told that this override does not apply to this client version return false, nil } return true, nil } func (s *operatingSystemStore) prepareQuery(d OSSpecifier) *gorm.DB { query := s.db.Model(&OperatingSystem{}) if d.Name != "" { query = query.Where("name = ? collate nocase OR release_id = ? collate nocase", d.Name, d.Name) } if d.LabelVersion != "" { query = query.Where("codename = ? collate nocase OR label_version = ? collate nocase", d.LabelVersion, d.LabelVersion) } if d.Channel != "" { query = query.Where("channel = ? collate nocase", d.Channel) } else { // we specifically want to match vanilla... query = query.Where("channel IS NULL OR channel = ''") } return query } func (s *operatingSystemStore) searchForOSExactVersions(query *gorm.DB, d OSSpecifier) ([]OperatingSystem, error) { var allOs []OperatingSystem handleQuery := func(q *gorm.DB, desc string) ([]OperatingSystem, error) { err := q.Find(&allOs).Error if err == nil { return allOs, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("failed to query distro by %s: %w", desc, err) } return nil, nil } if d.MajorVersion == "" && d.MinorVersion == "" { return handleQuery(query, "name and codename only") } // search by the most specific criteria first, then fallback var result []OperatingSystem var err error if d.MajorVersion != "" { if d.MinorVersion != "" { // non-empty major and minor versions specificQuery := query.Session(&gorm.Session{}).Where("major_version = ? AND minor_version = ?", d.MajorVersion, d.MinorVersion) result, err = handleQuery(specificQuery, "major and minor versions") if err != nil || len(result) > 0 { return result, err } } else { // empty minor version - exact match for major-only distros (e.g., Debian 8, 9, 10...) majorExclusiveQuery := query.Session(&gorm.Session{}).Where("major_version = ? AND minor_version = ?", d.MajorVersion, "") result, err = handleQuery(majorExclusiveQuery, "major version with empty minor") if err != nil || len(result) > 0 { return result, err } } // when fallback is disabled, don't try less specific version matches if d.DisableFallback { return nil, nil } // fallback to major version only, requiring the minor version to be blank. Note: it is important that we don't // match on any record with the given major version, we must only match on records that are intentionally empty // minor version. For instance, the DB may have rhel 8.1, 8.2, 8.3, 8.4, etc. We don't want to arbitrarily match // on one of these or match even the latest version, as even that may yield incorrect vulnerability matching // results. We are only intending to allow matches for when the vulnerability data is only specified at the major version level. majorExclusiveQuery := query.Session(&gorm.Session{}).Where("major_version = ? AND minor_version = ?", d.MajorVersion, "") result, err = handleQuery(majorExclusiveQuery, "exclusively major version") if err != nil || len(result) > 0 { return result, err } // fallback to major version for any minor version majorQuery := query.Session(&gorm.Session{}).Where("major_version = ?", d.MajorVersion) result, err = handleQuery(majorQuery, "major version with any minor version") if err != nil || len(result) > 0 { return result, err } } return allOs, nil } // UpdateOperatingSystemEOL updates the EOL and EOAS dates for operating systems // matching the given specifier. Returns the number of records updated. func (s *operatingSystemStore) UpdateOperatingSystemEOL(spec OSSpecifier, eolDate, eoasDate *time.Time) (int64, error) { spec.clean() // Build the query to find matching OS records query := s.db.Model(&OperatingSystem{}) if spec.Name != "" { query = query.Where("name = ? collate nocase", spec.Name) } if spec.MajorVersion != "" { query = query.Where("major_version = ?", spec.MajorVersion) } if spec.MinorVersion != "" { query = query.Where("minor_version = ?", spec.MinorVersion) } if spec.LabelVersion != "" { query = query.Where("codename = ? collate nocase OR label_version = ? collate nocase", spec.LabelVersion, spec.LabelVersion) } // Update the EOL fields updates := map[string]interface{}{ "eol_date": eolDate, "eoas_date": eoasDate, } result := query.Updates(updates) if result.Error != nil { return 0, fmt.Errorf("failed to update OS EOL data: %w", result.Error) } return result.RowsAffected, nil } func trimZeroes(s string) string { // trim leading zeros from the version components if s == "" { return s } if s[0] == '0' { s = strings.TrimLeft(s, "0") } if s == "" { // we've not only trimmed leading zeros, but also the entire string // we should preserve the zero value for the version return "0" } return s } ================================================ FILE: grype/db/v6/operating_system_store_test.go ================================================ package v6 import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/version" ) func TestOperatingSystemStore_ResolveOperatingSystem(t *testing.T) { // we always preload the OS aliases into the DB when staging for writing db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) ubuntu2004 := &OperatingSystem{Name: "ubuntu", ReleaseID: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal"} ubuntu2010 := &OperatingSystem{Name: "ubuntu", MajorVersion: "20", MinorVersion: "10", LabelVersion: "groovy"} rhel8 := &OperatingSystem{Name: "rhel", ReleaseID: "rhel", MajorVersion: "8"} rhel81 := &OperatingSystem{Name: "rhel", ReleaseID: "rhel", MajorVersion: "8", MinorVersion: "1"} debian10 := &OperatingSystem{Name: "debian", ReleaseID: "debian", MajorVersion: "10"} debian13 := &OperatingSystem{Name: "debian", ReleaseID: "debian", MajorVersion: "13", Codename: "trixie"} echo := &OperatingSystem{Name: "echo", ReleaseID: "echo", MajorVersion: "1"} alpine318 := &OperatingSystem{Name: "alpine", ReleaseID: "alpine", MajorVersion: "3", MinorVersion: "18"} alpineEdge := &OperatingSystem{Name: "alpine", ReleaseID: "alpine", LabelVersion: "edge"} debianUnstable := &OperatingSystem{Name: "debian", ReleaseID: "debian", LabelVersion: "unstable"} debian7 := &OperatingSystem{Name: "debian", ReleaseID: "debian", MajorVersion: "7", LabelVersion: "wheezy"} wolfi := &OperatingSystem{Name: "wolfi", ReleaseID: "wolfi", MajorVersion: "20230201"} arch := &OperatingSystem{Name: "archlinux", ReleaseID: "arch", MajorVersion: "20241110", MinorVersion: "0"} oracle5 := &OperatingSystem{Name: "oracle", ReleaseID: "ol", MajorVersion: "5"} oracle6 := &OperatingSystem{Name: "oracle", ReleaseID: "ol", MajorVersion: "6"} amazon2 := &OperatingSystem{Name: "amazon", ReleaseID: "amzn", MajorVersion: "2"} minimos := &OperatingSystem{Name: "minimos", ReleaseID: "minimos", MajorVersion: "20241031"} rocky8 := &OperatingSystem{Name: "rocky", ReleaseID: "rocky", MajorVersion: "8"} // should not be matched alma8 := &OperatingSystem{Name: "almalinux", ReleaseID: "almalinux", MajorVersion: "8"} // should not be matched operatingSystems := []*OperatingSystem{ ubuntu2004, ubuntu2010, rhel8, rhel81, debian10, debian13, alpine318, alpineEdge, debianUnstable, debian7, wolfi, arch, oracle5, oracle6, amazon2, minimos, rocky8, alma8, echo, } require.NoError(t, db.Create(&operatingSystems).Error) tests := []struct { name string os OSSpecifier expected []OperatingSystem expectErr require.ErrorAssertionFunc }{ { name: "specific distro with major and minor version", os: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }, expected: []OperatingSystem{*ubuntu2004}, }, { name: "specific distro with major and minor version (missing left padding)", os: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "4", }, expected: []OperatingSystem{*ubuntu2004}, }, { name: "alias resolution with major version", os: OSSpecifier{ Name: "centos", MajorVersion: "8", }, expected: []OperatingSystem{*rhel8}, }, { name: "alias resolution with major and minor version", os: OSSpecifier{ Name: "centos", MajorVersion: "8", MinorVersion: "1", }, expected: []OperatingSystem{*rhel81}, }, { name: "distro with major version only", os: OSSpecifier{ Name: "debian", MajorVersion: "10", }, expected: []OperatingSystem{*debian10}, }, { name: "codename resolution", os: OSSpecifier{ Name: "ubuntu", LabelVersion: "focal", }, expected: []OperatingSystem{*ubuntu2004}, }, { name: "codename and version info", os: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, expected: []OperatingSystem{*ubuntu2004}, }, { name: "conflicting codename and version info", os: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "fake", }, }, { name: "alpine edge version", os: OSSpecifier{ Name: "alpine", MajorVersion: "3", MinorVersion: "21", LabelVersion: "3.21.0_alpha20240807", }, expected: []OperatingSystem{*alpineEdge}, }, { name: "arch rolling variant", os: OSSpecifier{ Name: "arch", }, expected: []OperatingSystem{*arch}, }, { name: "wolfi rolling variant", os: OSSpecifier{ Name: "wolfi", MajorVersion: "20221018", }, expected: []OperatingSystem{*wolfi}, }, { name: "debian by codename for rolling alias", os: OSSpecifier{ Name: "debian", MajorVersion: "13", LabelVersion: "trixie", }, expected: []OperatingSystem{*debian13}, }, { name: "debian by codename for rolling alias", os: OSSpecifier{ Name: "debian", MajorVersion: "14", LabelVersion: "forky", }, expected: []OperatingSystem{*debianUnstable}, }, { name: "debian by codename", os: OSSpecifier{ Name: "debian", LabelVersion: "wheezy", }, expected: []OperatingSystem{*debian7}, }, { name: "debian by major version", os: OSSpecifier{ Name: "debian", MajorVersion: "7", }, expected: []OperatingSystem{*debian7}, }, { name: "debian by major.minor version", os: OSSpecifier{ Name: "debian", MajorVersion: "7", MinorVersion: "2", }, expected: []OperatingSystem{*debian7}, }, { name: "alpine with major and minor version", os: OSSpecifier{ Name: "alpine", MajorVersion: "3", MinorVersion: "18", }, expected: []OperatingSystem{*alpine318}, }, { name: "lookup by release ID (not name)", os: OSSpecifier{ Name: "ol", MajorVersion: "5", }, expected: []OperatingSystem{*oracle5}, }, { name: "lookup by non-standard name (oraclelinux)", os: OSSpecifier{ Name: "oraclelinux", // based on the grype distro names MajorVersion: "5", }, expected: []OperatingSystem{*oracle5}, }, { name: "lookup by non-standard name (amazonlinux)", os: OSSpecifier{ Name: "amazonlinux", // based on the grype distro names MajorVersion: "2", }, expected: []OperatingSystem{*amazon2}, }, { name: "lookup by non-standard name (oracle)", os: OSSpecifier{ Name: "oracle", MajorVersion: "5", }, expected: []OperatingSystem{*oracle5}, }, { name: "lookup by non-standard name (amazon)", os: OSSpecifier{ Name: "amazon", MajorVersion: "2", }, expected: []OperatingSystem{*amazon2}, }, { name: "lookup by non-standard name (rocky)", os: OSSpecifier{ Name: "rocky", MajorVersion: "8", }, expected: []OperatingSystem{*rhel8}, }, { name: "lookup by non-standard name (rockylinux)", os: OSSpecifier{ Name: "rockylinux", MajorVersion: "8", }, expected: []OperatingSystem{*rhel8}, }, { name: "lookup by non-standard name (alma)", os: OSSpecifier{ Name: "alma", MajorVersion: "8", }, expected: []OperatingSystem{*rhel8}, }, { name: "lookup by non-standard name (almalinux)", os: OSSpecifier{ Name: "almalinux", MajorVersion: "8", }, expected: []OperatingSystem{*rhel8}, }, { name: "echo rolling variant", os: OSSpecifier{ Name: "echo", MajorVersion: "1", }, expected: []OperatingSystem{*echo}, }, { name: "missing distro name", os: OSSpecifier{ MajorVersion: "8", }, expectErr: expectErrIs(t, ErrMissingOSIdentification), }, { name: "nonexistent distro", os: OSSpecifier{ Name: "madeup", MajorVersion: "99", }, }, { name: "minimos rolling variant", os: OSSpecifier{ Name: "minimos", }, expected: []OperatingSystem{*minimos}, }, { name: "nonexistent minor version falls back to major by default", os: OSSpecifier{ Name: "alpine", MajorVersion: "3", MinorVersion: "99", // doesn't exist, should fall back to 3.18 }, expected: []OperatingSystem{*alpine318}, }, { name: "nonexistent minor version with DisableFallback returns nothing", os: OSSpecifier{ Name: "alpine", MajorVersion: "3", MinorVersion: "99", // doesn't exist DisableFallback: true, // should NOT fall back }, expected: nil, }, { name: "major-only distro with DisableFallback finds exact match (Debian EOL lookup)", os: OSSpecifier{ Name: "debian", MajorVersion: "10", MinorVersion: "", // Debian uses major-only versions DisableFallback: true, }, expected: []OperatingSystem{*debian10}, }, { name: "RHEL major-only lookup finds major-only record (vuln matching)", os: OSSpecifier{ Name: "rhel", MajorVersion: "8", MinorVersion: "", // empty minor should find rhel8 directly }, expected: []OperatingSystem{*rhel8}, }, { name: "RHEL nonexistent minor falls back to major-only record (vuln matching)", os: OSSpecifier{ Name: "rhel", MajorVersion: "8", MinorVersion: "5", // 8.5 doesn't exist, should fall back to 8 }, expected: []OperatingSystem{*rhel8}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectErr == nil { tt.expectErr = require.NoError } result, err := oss.GetOperatingSystems(tt.os) tt.expectErr(t, err) if err != nil { return } if diff := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); diff != "" { t.Errorf("unexpected result (-want +got):\n%s", diff) } }) } } func TestOSSpecifier_String(t *testing.T) { tests := []struct { name string os *OSSpecifier expected string }{ { name: "nil distro", os: AnyOSSpecified, expected: "any", }, { name: "no distro specified", os: NoOSSpecified, expected: "none", }, { name: "only name specified", os: &OSSpecifier{ Name: "ubuntu", }, expected: "ubuntu", }, { name: "name and major version specified", os: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", }, expected: "ubuntu@20", }, { name: "name, major, and minor version specified", os: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }, expected: "ubuntu@20.04", }, { name: "name, major version, and codename specified", os: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", LabelVersion: "focal", }, expected: "ubuntu@20 (focal)", }, { name: "name and codename specified", os: &OSSpecifier{ Name: "ubuntu", LabelVersion: "focal", }, expected: "ubuntu@focal", }, { name: "name, major version, minor version, and codename specified", os: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, expected: "ubuntu@20.04", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.os.String() require.Equal(t, tt.expected, result) }) } } func TestTrimZeroes(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "empty string", input: "", expected: "", }, { name: "single zero", input: "0", expected: "0", }, { name: "multiple zeros only", input: "000", expected: "0", }, { name: "single non-zero digit", input: "5", expected: "5", }, { name: "no leading zeros", input: "123", expected: "123", }, { name: "single leading zero", input: "0123", expected: "123", }, { name: "multiple leading zeros", input: "000123", expected: "123", }, { name: "leading zeros with trailing zeros", input: "001230", expected: "1230", }, { name: "string starting with non-zero", input: "1000", expected: "1000", }, { name: "mixed digits with leading zeros", input: "00042", expected: "42", }, { name: "very long leading zeros", input: "00000000001", expected: "1", }, { name: "alphanumeric with leading zero", input: "0abc", expected: "abc", }, { name: "special characters with leading zeros", input: "00.123", expected: ".123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := trimZeroes(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestOSSpecifier_clean(t *testing.T) { tests := []struct { name string input OSSpecifier want OSSpecifier }{ { name: "trim 0s", input: OSSpecifier{ Name: "Ubuntu", MajorVersion: "20", MinorVersion: "04", }, want: OSSpecifier{ Name: "Ubuntu", MajorVersion: "20", MinorVersion: "4", }, }, { name: "preserve 0 value", input: OSSpecifier{ Name: "Redhat", MajorVersion: "9", MinorVersion: "0", }, want: OSSpecifier{ Name: "Redhat", MajorVersion: "9", MinorVersion: "0", // important! ...9 != 9.0 since 9 includes multiple minor versions }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := tt.input o.clean() if d := cmp.Diff(tt.want, o); d != "" { t.Errorf("OSSpecifier.clean() mismatch (-want +got):\n%s", d) } }) } } func TestApplyOverride(t *testing.T) { tests := []struct { name string osSpecifier OSSpecifier override OperatingSystemSpecifierOverride expected OSSpecifier wantApplied bool }{ { name: "replace name", osSpecifier: OSSpecifier{ Name: "centos", MajorVersion: "8", MinorVersion: "1", }, override: OperatingSystemSpecifierOverride{ ReplacementName: strPtr("rhel"), }, expected: OSSpecifier{ Name: "rhel", MajorVersion: "8", MinorVersion: "1", }, wantApplied: true, }, { name: "replace major version", osSpecifier: OSSpecifier{ Name: "rhel", MajorVersion: "8", MinorVersion: "1", }, override: OperatingSystemSpecifierOverride{ ReplacementMajorVersion: strPtr("9"), }, expected: OSSpecifier{ Name: "rhel", MajorVersion: "9", MinorVersion: "1", }, wantApplied: true, }, { name: "replace minor version", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }, override: OperatingSystemSpecifierOverride{ ReplacementMinorVersion: strPtr("10"), }, expected: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "10", }, wantApplied: true, }, { name: "replace label version", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, override: OperatingSystemSpecifierOverride{ ReplacementLabelVersion: strPtr("jammy"), }, expected: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "jammy", }, wantApplied: true, }, { name: "replace channel", osSpecifier: OSSpecifier{ Name: "rhel", MajorVersion: "9", MinorVersion: "1", Channel: "eeus", }, override: OperatingSystemSpecifierOverride{ ReplacementChannel: strPtr("eus"), }, expected: OSSpecifier{ Name: "rhel", MajorVersion: "9", MinorVersion: "1", Channel: "eus", }, wantApplied: true, }, { name: "rolling flag clears versions", osSpecifier: OSSpecifier{ Name: "arch", MajorVersion: "2024", MinorVersion: "01", LabelVersion: "rolling", }, override: OperatingSystemSpecifierOverride{ Rolling: true, }, expected: OSSpecifier{ Name: "arch", MajorVersion: "", MinorVersion: "", LabelVersion: "rolling", }, wantApplied: true, }, { name: "comprehensive override - all fields", osSpecifier: OSSpecifier{ Name: "centos", MajorVersion: "7", MinorVersion: "5", LabelVersion: "core", }, override: OperatingSystemSpecifierOverride{ ReplacementName: strPtr("rhel"), ReplacementMajorVersion: strPtr("7"), ReplacementMinorVersion: strPtr("9"), ReplacementLabelVersion: strPtr("server"), }, expected: OSSpecifier{ Name: "rhel", MajorVersion: "7", MinorVersion: "9", LabelVersion: "server", }, wantApplied: true, }, { name: "no replacement fields - no changes", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, override: OperatingSystemSpecifierOverride{ Alias: "ubuntu", }, expected: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, wantApplied: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // make a copy to avoid modifying the original d := tt.osSpecifier applied := applyOverride(&d, tt.override) require.Equal(t, tt.wantApplied, applied) if diff := cmp.Diff(tt.expected, d); diff != "" { t.Errorf("unexpected result (-want +got):\n%s", diff) } }) } } func TestApplyOSSpecifierOverrides(t *testing.T) { tests := []struct { name string osSpecifier OSSpecifier aliases []OperatingSystemSpecifierOverride clientVersion *version.Version wantErr require.ErrorAssertionFunc expected OSSpecifier }{ { name: "no aliases - no change", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, aliases: []OperatingSystemSpecifierOverride{}, expected: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, }, { name: "multiple overrides - first match wins", osSpecifier: OSSpecifier{ Name: "centos", MajorVersion: "8", MinorVersion: "1", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "centos", ReplacementName: strPtr("rhel"), }, { Alias: "centos", ReplacementName: strPtr("fedora"), }, }, expected: OSSpecifier{ Name: "rhel", // overridden MajorVersion: "8", MinorVersion: "1", }, }, { name: "codename mismatch - no override", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "ubuntu", Codename: "jammy", ReplacementName: strPtr("ubuntu-lts"), }, }, expected: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", // not overridden }, }, { name: "version mismatch - no override", osSpecifier: OSSpecifier{ Name: "debian", MajorVersion: "10", MinorVersion: "5", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "debian", Version: "11.0", ReplacementName: strPtr("debian-bullseye"), }, }, expected: OSSpecifier{ Name: "debian", MajorVersion: "10", // not overridden MinorVersion: "5", // not overridden }, }, { name: "version pattern mismatch - no override", osSpecifier: OSSpecifier{ Name: "alpine", MajorVersion: "2", MinorVersion: "18", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "alpine", VersionPattern: "^3\\.[0-9]+$", ReplacementName: strPtr("alpine-stable"), }, }, expected: OSSpecifier{ Name: "alpine", // not overridden MajorVersion: "2", MinorVersion: "18", }, }, { name: "client version constraint satisfied", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "ubuntu", ApplicableClientDBSchemas: ">=1.0.0", ReplacementName: strPtr("ubuntu-new"), }, }, clientVersion: version.New("1.2.0", version.SemanticFormat), // matches the constraint, thus allowed to override expected: OSSpecifier{ Name: "ubuntu-new", // overridden MajorVersion: "20", MinorVersion: "04", }, }, { name: "client version constraint not satisfied", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "ubuntu", ApplicableClientDBSchemas: ">=2.0.0", // does not match the client version, thus no override ReplacementName: strPtr("ubuntu-new"), }, }, clientVersion: version.New("1.2.0", version.SemanticFormat), expected: OSSpecifier{ Name: "ubuntu", // not overridden MajorVersion: "20", MinorVersion: "04", }, }, { name: "invalid client version constraint - honor the override", osSpecifier: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }, aliases: []OperatingSystemSpecifierOverride{ { Alias: "ubuntu", ApplicableClientDBSchemas: "invalid-constraint", // oops! ReplacementName: strPtr("ubuntu-new"), }, }, clientVersion: version.New("1.2.0", version.SemanticFormat), expected: OSSpecifier{ Name: "ubuntu-new", // overridden MajorVersion: "20", MinorVersion: "04", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } // make a copy to avoid modifying the original d := tt.osSpecifier err := applyOSSpecifierOverrides(&d, tt.aliases, tt.clientVersion) tt.wantErr(t, err) if err != nil { return } if diff := cmp.Diff(tt.expected, d); diff != "" { t.Errorf("unexpected result (-want +got):\n%s", diff) } }) } } func strPtr(s string) *string { return &s } ================================================ FILE: grype/db/v6/package_store.go ================================================ package v6 import ( "errors" "fmt" "strings" "time" "golang.org/x/exp/maps" "gorm.io/gorm" "gorm.io/gorm/clause" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) const ( anyPkg = "any" anyOS = "any" ) var NoOSSpecified = &OSSpecifier{} var AnyOSSpecified *OSSpecifier var AnyPackageSpecified *PackageSpecifier var ErrMissingOSIdentification = errors.New("missing OS name or codename") var ErrOSNotPresent = errors.New("OS not present") var ErrLimitReached = errors.New("query limit reached") type GetPackageOptions struct { PreloadOS bool PreloadPackage bool PreloadPackageCPEs bool PreloadVulnerability bool PreloadBlob bool OSs OSSpecifiers Vulnerabilities VulnerabilitySpecifiers AllowBroadCPEMatching bool Limit int } type PackageSpecifiers []*PackageSpecifier type PackageSpecifier struct { Name string Ecosystem string CPE *cpe.Attributes } func (p *PackageSpecifier) String() string { if p == nil { return anyPkg } var args []string if p.Name != "" { args = append(args, fmt.Sprintf("name=%s", p.Name)) } if p.Ecosystem != "" { args = append(args, fmt.Sprintf("ecosystem=%s", p.Ecosystem)) } if p.CPE != nil { args = append(args, fmt.Sprintf("cpe=%s", p.CPE.String())) } if len(args) > 0 { return fmt.Sprintf("package(%s)", strings.Join(args, ", ")) } return anyPkg } func (p PackageSpecifiers) String() string { if len(p) == 0 { return anyPkg } var parts []string for _, v := range p { parts = append(parts, v.String()) } return strings.Join(parts, ", ") } type packageHandleStore interface { *AffectedPackageHandle | *UnaffectedPackageHandle } type packageHandleAccessor interface { getPackageHandle() *packageHandle } type packageStore struct { db *gorm.DB blobStore *blobStore osStore *operatingSystemStore } func newPackageStore(db *gorm.DB, bs *blobStore, oss *operatingSystemStore) *packageStore { return &packageStore{ db: db, blobStore: bs, osStore: oss, } } func addPackages[T packageHandleStore](s *packageStore, packages ...T) (bool, error) { cacheInst, ok := cacheFromContext(s.db.Statement.Context) if !ok { return false, fmt.Errorf("unable to fetch package cache from context") } var final []*Package var hasCPEs bool byCacheKey := make(map[string][]*Package) for _, p := range packages { // convert to packageHandle to access fields ph := any(p).(packageHandleAccessor).getPackageHandle() if ph.Package != nil { if len(ph.Package.CPEs) > 0 { // never use the cache if there are CPEs involved final = append(final, ph.Package) hasCPEs = true continue } key := ph.Package.cacheKey() if existingID, ok := cacheInst.getID(ph.Package); ok { // seen in a previous transaction... ph.PackageID = existingID } else if _, ok := byCacheKey[key]; !ok { // not seen within this transaction final = append(final, ph.Package) } byCacheKey[key] = append(byCacheKey[key], ph.Package) } } if len(final) == 0 { return false, nil } // since there is risk of needing to write through packages with conflicting CPEs we cannot write these in batches, // and since the before hooks reason about previous entries within this loop (potentially) we must ensure that // these are written in different transactions. for _, p := range final { if err := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(p).Error; err != nil { return false, fmt.Errorf("unable to create package records: %w", err) } } // update the cache with the new records for _, r := range final { cacheInst.set(r) } // update all references with the IDs from the cache for _, refs := range byCacheKey { for _, r := range refs { id, ok := cacheInst.getID(r) if ok { r.setRowID(id) } } } // update the parent objects with the FK ID for _, p := range packages { ph := any(p).(packageHandleAccessor).getPackageHandle() if ph.Package != nil { ph.PackageID = ph.Package.ID } } return hasCPEs, nil } func addPackagesWithOS[T packageHandleStore](s *packageStore, packages ...T) error { for _, p := range packages { ph := any(p).(packageHandleAccessor).getPackageHandle() if err := s.osStore.addOsFromPackages(ph); err != nil { return fmt.Errorf("unable to add package OS: %w", err) } } hasCpes, err := addPackages(s, packages...) if err != nil { return fmt.Errorf("unable to add packages: %w", err) } omit := []string{"OperatingSystem"} if !hasCpes { omit = append(omit, "Package") } for _, v := range packages { if err := s.blobStore.addBlobable(any(v).(blobable)); err != nil { return fmt.Errorf("unable to add blob: %w", err) } if err := s.db.Omit(omit...).Create(v).Error; err != nil { return err } } return nil } func getPackages[T packageHandleStore]( //nolint:funlen s *packageStore, pkg *PackageSpecifier, config *GetPackageOptions, tableName string, ) ([]T, error) { if config == nil { config = &GetPackageOptions{} } start := time.Now() count := 0 defer func() { log. WithFields( "pkg", pkg.String(), "distro", config.OSs, "vulns", config.Vulnerabilities, "duration", time.Since(start), "records", count, ). Trace("fetched package record") }() query := s.handlePackage(s.db.Table(tableName), pkg, config.AllowBroadCPEMatching) var err error query, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities, tableName) if err != nil { return nil, err } query, err = s.handleOSOptions(query, config.OSs, tableName) if err != nil { return nil, err } query = s.handlePreload(query, *config) var models []T var results []T if err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { if config.PreloadBlob { var blobs []blobable for _, r := range results { blobs = append(blobs, any(r).(blobable)) } if err := s.blobStore.attachBlobValue(blobs...); err != nil { return fmt.Errorf("unable to attach package blobs: %w", err) } } if config.PreloadVulnerability { var vulns []blobable for _, r := range results { ph := any(r).(packageHandleAccessor).getPackageHandle() if ph.Vulnerability != nil { vulns = append(vulns, ph.Vulnerability) } } if err := s.blobStore.attachBlobValue(vulns...); err != nil { return fmt.Errorf("unable to attach vulnerability blob: %w", err) } } models = append(models, results...) count += len(results) if config.Limit > 0 && len(models) >= config.Limit { return ErrLimitReached } return nil }).Error; err != nil { return models, fmt.Errorf("unable to fetch package records: %w", err) } return models, nil } func (s *packageStore) handlePackage(query *gorm.DB, p *PackageSpecifier, allowBroad bool) *gorm.DB { if p == nil { return query } if err := s.applyPackageAlias(p); err != nil { log.Errorf("failed to apply package alias: %v", err) } // Get table name from the query tableName := query.Statement.Table query = query.Joins(fmt.Sprintf("JOIN packages ON %s.package_id = packages.id", tableName)) if p.Name != "" { query = query.Where("packages.name = ? collate nocase", p.Name) } if p.Ecosystem != "" { query = query.Where("packages.ecosystem = ? collate nocase", p.Ecosystem) } if p.CPE != nil { query = query.Joins("JOIN package_cpes ON packages.id = package_cpes.package_id") query = query.Joins("JOIN cpes ON package_cpes.cpe_id = cpes.id") query = handleCPEOptions(query, p.CPE, allowBroad) } return query } func (s *packageStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier, tableName string) (*gorm.DB, error) { if len(configs) == 0 { return query, nil } query = query.Joins(fmt.Sprintf("JOIN vulnerability_handles ON %s.vulnerability_id = vulnerability_handles.id", tableName)) return handleVulnerabilityOptions(s.db, query, configs...) } func (s *packageStore) handleOSOptions(query *gorm.DB, configs []*OSSpecifier, tableName string) (*gorm.DB, error) { ids := map[int64]struct{}{} if len(configs) == 0 { configs = append(configs, AnyOSSpecified) } var hasAny, hasNone, hasSpecific bool // process OS specs... for _, config := range configs { switch { case hasOSSpecified(config): curResolved, err := s.osStore.GetOperatingSystems(*config) if err != nil { return nil, fmt.Errorf("unable to resolve operating system: %w", err) } hasSpecific = true for _, d := range curResolved { ids[int64(d.ID)] = struct{}{} } case config == AnyOSSpecified: hasAny = true case *config == *NoOSSpecified: hasNone = true } } if (hasAny || hasNone) && hasSpecific { return nil, fmt.Errorf("cannot mix specific OS with 'any' or 'none' OS specifiers") } switch { case hasAny: return query, nil case hasNone: return query.Where("operating_system_id IS NULL"), nil } // we were told to filter by specific OSes but found no matching OSes... if len(ids) == 0 { return nil, ErrOSNotPresent } query = query.Where(fmt.Sprintf("%s.operating_system_id IN ?", tableName), maps.Keys(ids)) return query, nil } // Keep the original helper methods unchanged func (s *packageStore) applyPackageAlias(d *PackageSpecifier) error { if d.Ecosystem == "" { return nil } // only ecosystem replacement is supported today var aliases []PackageSpecifierOverride err := s.db.Where("ecosystem = ? collate nocase", d.Ecosystem).Find(&aliases).Error if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("failed to resolve alias for distro %q: %w", d.Name, err) } return nil } var alias *PackageSpecifierOverride for _, a := range aliases { if a.Ecosystem == "" { continue } alias = &a break } if alias == nil { return nil } if alias.ReplacementEcosystem != nil { d.Ecosystem = *alias.ReplacementEcosystem } return nil } func (s *packageStore) handlePreload(query *gorm.DB, config GetPackageOptions) *gorm.DB { var limitArgs []interface{} if config.Limit > 0 { query = query.Limit(config.Limit) limitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB { return db.Limit(config.Limit) }) } if config.PreloadPackage { query = query.Preload("Package", limitArgs...) if config.PreloadPackageCPEs { query = query.Preload("Package.CPEs", limitArgs...) } } if config.PreloadVulnerability { query = query.Preload("Vulnerability", limitArgs...).Preload("Vulnerability.Provider", limitArgs...) } if config.PreloadOS { query = query.Preload("OperatingSystem", limitArgs...) } return query } func handleCPEOptions(query *gorm.DB, c *cpe.Attributes, allowBroad bool) *gorm.DB { query = queryCPEAttributeScope(query, c.Part, "cpes.part", allowBroad) query = queryCPEAttributeScope(query, c.Vendor, "cpes.vendor", allowBroad) query = queryCPEAttributeScope(query, c.Product, "cpes.product", allowBroad) query = queryCPEAttributeScope(query, c.Edition, "cpes.edition", allowBroad) query = queryCPEAttributeScope(query, c.Language, "cpes.language", allowBroad) query = queryCPEAttributeScope(query, c.SWEdition, "cpes.software_edition", allowBroad) query = queryCPEAttributeScope(query, c.TargetSW, "cpes.target_software", allowBroad) query = queryCPEAttributeScope(query, c.TargetHW, "cpes.target_hardware", allowBroad) query = queryCPEAttributeScope(query, c.Other, "cpes.other", allowBroad) return query } func queryCPEAttributeScope(query *gorm.DB, value string, dbColumn string, allowBroad bool) *gorm.DB { if value == cpe.Any { return query } if allowBroad { // this allows for a package that specifies a CPE like // // 'cpe:2.3:a:cloudflare:octorpki:1.4.1:*:*:*:*:golang:*:*' // // to be able to positively match with a package CPE that claims to match "any" target software. // // 'cpe:2.3:a:cloudflare:octorpki:1.4.1:*:*:*:*:*:*:*' // // practically speaking, how would a vulnerability provider know that the package is vulnerable for all // target software values (against the universe of packaging) -- this isn't practical. return query.Where(fmt.Sprintf("%s = ? collate nocase or %s = ? collate nocase", dbColumn, dbColumn), value, cpe.Any) } // this is the most practical use case, where the package CPE with specified values must match the vulnerability // CPE exactly (only for specified fields) return query.Where(fmt.Sprintf("%s = ? collate nocase", dbColumn), value) } func hasOSSpecified(d *OSSpecifier) bool { if d == AnyOSSpecified { return false } if *d == *NoOSSpecified { return false } return true } ================================================ FILE: grype/db/v6/provider_store.go ================================================ package v6 import ( "fmt" "sort" "gorm.io/gorm" "github.com/anchore/grype/internal/log" ) type ProviderStoreReader interface { GetProvider(name string) (*Provider, error) AllProviders() ([]Provider, error) fillProviders(handles []ref[string, Provider]) error } type ProviderStoreWriter interface { AddProvider(p Provider) error } type providerStore struct { db *gorm.DB } func newProviderStore(db *gorm.DB) *providerStore { return &providerStore{ db: db, } } func (s *providerStore) AddProvider(p Provider) error { result := s.db.FirstOrCreate(&p) if result.Error != nil { return fmt.Errorf("failed to create provider record: %w", result.Error) } return nil } func (s *providerStore) GetProvider(name string) (*Provider, error) { log.WithFields("name", name).Trace("fetching provider record") var provider Provider result := s.db.Where("id = ?", name).First(&provider) if result.Error != nil { return nil, fmt.Errorf("failed to fetch provider (name=%q): %w", name, result.Error) } return &provider, nil } func (s *providerStore) AllProviders() ([]Provider, error) { log.Trace("fetching all provider records") var providers []Provider result := s.db.Find(&providers) if result.Error != nil { return nil, fmt.Errorf("failed to fetch all providers: %w", result.Error) } sort.Slice(providers, func(i, j int) bool { return providers[i].ID < providers[j].ID }) return providers, nil } func (s *providerStore) fillProviders(handles []ref[string, Provider]) error { providers, err := s.AllProviders() if err != nil { return err } providerMap := make(map[string]*Provider) for _, provider := range providers { providerMap[provider.ID] = &provider } for _, handle := range handles { if handle.id == nil { continue } *handle.ref = providerMap[*handle.id] } return nil } ================================================ FILE: grype/db/v6/provider_store_test.go ================================================ package v6 import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestProviderStore(t *testing.T) { now := time.Date(2021, 1, 1, 2, 3, 4, 5, time.UTC) tests := []struct { name string providers []Provider wantErr require.ErrorAssertionFunc }{ { name: "add new provider", providers: []Provider{ { ID: "ubuntu", Version: "1.0", Processor: "vunnel", DateCaptured: &now, InputDigest: "sha256:abcd1234", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { db := setupTestStore(t).db s := newProviderStore(db) if tt.wantErr == nil { tt.wantErr = require.NoError } for i := range tt.providers { p := tt.providers[i] // note: we always write providers via the vulnerability handle (there is no store adder) vuln := VulnerabilityHandle{ Name: "CVE-1234-5678", Provider: &p, } isLast := i == len(tt.providers)-1 err := db.Create(&vuln).Error if !isLast { require.NoError(t, err) continue } tt.wantErr(t, err) if err != nil { continue } provider, err := s.GetProvider(p.ID) tt.wantErr(t, err) if err != nil { assert.Nil(t, provider) return } require.NoError(t, err) require.NotNil(t, provider) if d := cmp.Diff(p, *provider); d != "" { t.Errorf("unexpected provider (-want +got): %s", d) } } }) } } func TestProviderStore_GetProvider(t *testing.T) { s := newProviderStore(setupTestStore(t).db) p, err := s.GetProvider("fake") require.Error(t, err) assert.Nil(t, p) } ================================================ FILE: grype/db/v6/refs.go ================================================ package v6 import ( "slices" "gorm.io/gorm" ) type ref[ID, T any] struct { id *ID ref **T } type idRef[T any] ref[ID, T] type refProvider[T, R any] func(*T) idRef[R] type idProvider[T any] func(*T) ID func fillRefs[T, R any](reader Reader, handles []*T, getRef refProvider[T, R], refID idProvider[R]) error { if len(handles) == 0 { return nil } // collect all ref locations and IDs var refs []idRef[R] var ids []ID for i := range handles { ref := getRef(handles[i]) if ref.id == nil { continue } refs = append(refs, ref) id := *ref.id if slices.Contains(ids, id) { continue } ids = append(ids, id) } // load a map with all id -> ref results var values []R tx := reader.(lowLevelReader).GetDB().Where("id IN (?)", ids) err := tx.Find(&values).Error if err != nil { return err } refsByID := map[ID]*R{} for i := range values { v := &values[i] id := refID(v) refsByID[id] = v } // assign matching refs back to the object graph for _, ref := range refs { if ref.id == nil { continue } incomingRef := refsByID[*ref.id] *ref.ref = incomingRef } return nil } // ptrs returns a slice of pointers to each element in the provided slice func ptrs[T any](values []T) []*T { if len(values) == 0 { return nil } out := make([]*T, len(values)) for i := range values { out[i] = &values[i] } return out } type lowLevelReader interface { GetDB() *gorm.DB } ================================================ FILE: grype/db/v6/schema/main.go ================================================ package main import ( "bytes" "database/sql" "encoding/json" "fmt" "go/ast" "io" "os" "os/exec" "path/filepath" "reflect" "sort" "strings" "github.com/invopop/jsonschema" "golang.org/x/tools/go/packages" v6 "github.com/anchore/grype/grype/db/v6" ) func main() { // The schema version is derived from the database version version := fmt.Sprintf("%d.%d.%d", v6.ModelVersion, v6.Revision, v6.Addition) pkgPatterns := []string{".."} comments := parseCommentsFromPackages(pkgPatterns) fmt.Printf("Extracted field comments from %d structs\n", len(comments)) // Generate SQL schema if err := generateSQLSchema(version); err != nil { fmt.Printf("Failed to generate SQL schema: %v\n", err) os.Exit(1) } // Generate unified blob JSON schema err := generateBlobSchema(version, comments) if err != nil { fmt.Printf("Failed to generate blob JSON schema: %v\n", err) os.Exit(1) } } func generateSQLSchema(version string) error { // Create an in-memory database with all models db, err := v6.NewLowLevelDB("", true, true, false) if err != nil { return fmt.Errorf("failed to create database: %w", err) } sqlDB, err := db.DB() if err != nil { return fmt.Errorf("failed to get underlying database: %w", err) } defer sqlDB.Close() var schema strings.Builder schema.WriteString("-- Generated by grype/db/v6/schema\n") schema.WriteString("-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\n") fmt.Fprintf(&schema, "-- Schema version: %s\n\n", version) // Query for all tables and their CREATE statements createStatements, err := querySchemaSorted(sqlDB, "table") if err != nil { return err } for _, stmt := range createStatements { // Normalize the CREATE TABLE statement to ensure deterministic output normalized := normalizeCreateTable(stmt) schema.WriteString(normalized) schema.WriteString(";\n\n") } // Get all indexes indexStatements, err := querySchemaSorted(sqlDB, "index") if err != nil { return err } if len(indexStatements) > 0 { schema.WriteString("-- Indexes\n") for _, stmt := range indexStatements { schema.WriteString(stmt) schema.WriteString(";\n\n") } } return writeFile(schema.String(), "db/sql", version, ".sql") } func normalizeCreateTable(stmt string) string { // Sort CONSTRAINT clauses within CREATE TABLE statements for deterministic output // Foreign keys can appear in non-deterministic order from SQLite // Find the constraints section if !strings.Contains(stmt, "CONSTRAINT") { return stmt } // Split by CONSTRAINT keyword parts := strings.Split(stmt, "CONSTRAINT") if len(parts) <= 1 { return stmt } // First part contains everything before constraints prefix := parts[0] // Collect all constraints var constraints []string for i := 1; i < len(parts); i++ { constraints = append(constraints, "CONSTRAINT"+parts[i]) } // Sort constraints sort.Strings(constraints) // Rebuild: need to handle the last column/field before constraints // The prefix ends with a comma, and each constraint except the last should have a comma result := strings.TrimRight(prefix, ",") for _, constraint := range constraints { result += "," // Remove trailing comma or closing paren from constraint constraint = strings.TrimRight(constraint, ",)") result += constraint } result += ")" return result } func querySchemaSorted(db *sql.DB, objectType string) ([]string, error) { // Use a placeholder '?' to prevent SQL injection warnings (gosec G201) query := ` SELECT sql FROM sqlite_master WHERE type = ? AND name NOT LIKE 'sqlite_%%' ` if objectType == "index" { query += " AND sql IS NOT NULL" } query += " ORDER BY name" // Pass the variable as a parameter to the query function rows, err := db.Query(query, objectType) if err != nil { return nil, fmt.Errorf("failed to query schema for type %s: %w", objectType, err) } defer rows.Close() var statements []string for rows.Next() { var sql string if err := rows.Scan(&sql); err != nil { return nil, fmt.Errorf("failed to scan schema: %w", err) } statements = append(statements, sql) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating schema: %w", err) } // Sort for deterministic output sort.Strings(statements) return statements, nil } func generateBlobSchema(version string, comments map[string]map[string]string) error { // Create a unified schema that includes all blob types schema := buildUnifiedBlobSchema(version, comments) encoded := encode(schema) return writeFile(string(encoded), "db/blob/json", version, ".json") } func buildUnifiedBlobSchema(version string, comments map[string]map[string]string) *jsonschema.Schema { reflector := &jsonschema.Reflector{ AllowAdditionalProperties: true, Namer: func(r reflect.Type) string { return strings.TrimPrefix(r.Name(), "JSON") }, } // Reflect all three blob types to get their definitions vulnBlobSchema := reflector.ReflectFromType(reflect.TypeOf(v6.VulnerabilityBlob{})) packageBlobSchema := reflector.ReflectFromType(reflect.TypeOf(v6.PackageBlob{})) kevBlobSchema := reflector.ReflectFromType(reflect.TypeOf(v6.KnownExploitedVulnerabilityBlob{})) // Create the unified schema with oneOf unifiedSchema := &jsonschema.Schema{ Version: jsonschema.Version, ID: jsonschema.ID(fmt.Sprintf("anchore.io/schema/grype/db/blob/json/%s", version)), Description: "Unified schema for all blob types stored in the Grype v6 database", OneOf: []*jsonschema.Schema{ {Ref: "#/$defs/VulnerabilityBlob"}, {Ref: "#/$defs/PackageBlob"}, {Ref: "#/$defs/KnownExploitedVulnerabilityBlob"}, }, Definitions: make(map[string]*jsonschema.Schema), } // Merge all definitions from the three schemas mergeDefinitions(unifiedSchema.Definitions, vulnBlobSchema.Definitions) mergeDefinitions(unifiedSchema.Definitions, packageBlobSchema.Definitions) mergeDefinitions(unifiedSchema.Definitions, kevBlobSchema.Definitions) // Apply comments to the definitions applyComments(unifiedSchema.Definitions, comments) return unifiedSchema } func mergeDefinitions(target, source map[string]*jsonschema.Schema) { for k, v := range source { target[k] = v } } func applyComments(definitions map[string]*jsonschema.Schema, comments map[string]map[string]string) { for structName, fields := range comments { if structSchema, exists := definitions[structName]; exists { if structSchema.Definitions == nil { structSchema.Definitions = make(map[string]*jsonschema.Schema) } for fieldName, comment := range fields { if fieldName == "" { // struct-level comment structSchema.Description = comment continue } // field level comment if comment == "" { continue } if _, exists := structSchema.Properties.Get(fieldName); exists { fieldSchema, exists := structSchema.Definitions[fieldName] if exists { fieldSchema.Description = comment } else { fieldSchema = &jsonschema.Schema{ Description: comment, } } structSchema.Definitions[fieldName] = fieldSchema } } } } } func encode(schema *jsonschema.Schema) []byte { newSchemaBuffer := new(bytes.Buffer) enc := json.NewEncoder(newSchemaBuffer) // prevent > and < from being escaped in the payload enc.SetEscapeHTML(false) enc.SetIndent("", " ") err := enc.Encode(&schema) if err != nil { panic(err) } return newSchemaBuffer.Bytes() } func writeFile(content, component, version, extension string) error { parent := filepath.Join(repoRoot(), "schema", "grype", component) schemaPath := filepath.Join(parent, fmt.Sprintf("schema-%s%s", version, extension)) latestSchemaPath := filepath.Join(parent, fmt.Sprintf("schema-latest%s", extension)) // Create parent directory if it doesn't exist if err := os.MkdirAll(parent, 0o755); err != nil { return fmt.Errorf("unable to create schema directory: %w", err) } if _, err := os.Stat(schemaPath); !os.IsNotExist(err) { // check if the schema is the same... existingFh, err := os.Open(schemaPath) if err != nil { return err } defer existingFh.Close() existingBytes, err := io.ReadAll(existingFh) if err != nil { return err } if string(existingBytes) == content { // the generated schema is the same, bail with no error :) fmt.Printf("No change to the existing %q schema!\n", component) return nil } // the generated schema is different, bail with error :( fmt.Printf("Cowardly refusing to overwrite existing %q schema (%s)!\n", component, schemaPath) fmt.Printf("The schema has changed but the version has not been incremented.\n") fmt.Printf("See grype/db/v6/db.go to increment the ModelVersion, Revision, or Addition constants.\n") return fmt.Errorf("refusing to overwrite existing schema") } fh, err := os.Create(schemaPath) if err != nil { return err } defer fh.Close() if _, err = fh.WriteString(content); err != nil { return err } latestFile, err := os.Create(latestSchemaPath) if err != nil { return err } defer latestFile.Close() if _, err = latestFile.WriteString(content); err != nil { return err } fmt.Printf("Wrote new %q schema to %q\n", component, schemaPath) return nil } // parseCommentsFromPackages scans multiple packages and collects field comments for structs. func parseCommentsFromPackages(pkgPatterns []string) map[string]map[string]string { commentMap := make(map[string]map[string]string) cfg := &packages.Config{ Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports, } pkgs, err := packages.Load(cfg, pkgPatterns...) if err != nil { panic(fmt.Errorf("failed to load packages: %w", err)) } for _, pkg := range pkgs { for _, file := range pkg.Syntax { fileComments := parseFileComments(file) for structName, fields := range fileComments { if _, exists := commentMap[structName]; !exists { commentMap[structName] = fields } } } } return commentMap } // parseFileComments extracts comments for structs and their fields in a single file. func parseFileComments(node *ast.File) map[string]map[string]string { commentMap := make(map[string]map[string]string) ast.Inspect(node, func(n ast.Node) bool { ts, ok := n.(*ast.TypeSpec) if !ok { return true } st, ok := ts.Type.(*ast.StructType) if !ok { return true } structName := ts.Name.Name fieldComments := make(map[string]string) // extract struct-level comment if ts.Doc != nil { structComment := strings.TrimSpace(ts.Doc.Text()) if !strings.Contains(structComment, "TODO:") { fieldComments[""] = cleanComment(structComment) } } // extract field-level comments for _, field := range st.Fields.List { if len(field.Names) == 0 { continue } fieldName := field.Names[0].Name jsonTag := getJSONTag(field) if field.Doc != nil { comment := strings.TrimSpace(field.Doc.Text()) if strings.Contains(comment, "TODO:") { continue } if jsonTag != "" { fieldComments[jsonTag] = cleanComment(comment) } else { fieldComments[fieldName] = cleanComment(comment) } } } if len(fieldComments) > 0 { commentMap[structName] = fieldComments } return true }) return commentMap } func cleanComment(comment string) string { // remove the first word, since that is the field name (if following go-doc patterns) split := strings.SplitN(comment, " ", 2) if len(split) > 1 { comment = split[1] } return strings.TrimSpace(strings.ReplaceAll(comment, "\"", "'")) } func getJSONTag(field *ast.Field) string { if field.Tag != nil { tagValue := strings.Trim(field.Tag.Value, "`") structTag := reflect.StructTag(tagValue) if jsonTag, ok := structTag.Lookup("json"); ok { jsonParts := strings.Split(jsonTag, ",") return strings.TrimSpace(jsonParts[0]) } } return "" } func repoRoot() string { root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { panic(fmt.Errorf("unable to find repo root dir: %+v", err)) } absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) if err != nil { panic(fmt.Errorf("unable to get abs path to repo root: %w", err)) } return absRepoRoot } ================================================ FILE: grype/db/v6/search_query.go ================================================ package v6 import ( "fmt" "github.com/anchore/grype/grype/db/v6/name" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) // searchQuery holds the parsed criteria and search parameters type searchQuery struct { pkgSpec *PackageSpecifier cpeSpec *cpe.Attributes osSpecs OSSpecifiers vulnSpecs VulnerabilitySpecifiers pkgType syftPkg.Type versionMatcher search.VersionConstraintMatcher unaffectedOnly bool } func newSearchQuery(criteriaSet []vulnerability.Criteria) (*searchQuery, []vulnerability.Criteria, error) { builder := newSearchQueryBuilder() if err := builder.ApplyCriteria(criteriaSet); err != nil { return nil, nil, err } return builder.Build() } // searchQueryBuilder provides a structured way to build searchQuery objects // from vulnerability criteria, replacing the large switch statement with focused handler methods. type searchQueryBuilder struct { query *searchQuery remainingCriteria []vulnerability.Criteria } // newSearchQueryBuilder creates a new searchQueryBuilder with an empty query func newSearchQueryBuilder() *searchQueryBuilder { return &searchQueryBuilder{ query: &searchQuery{}, remainingCriteria: make([]vulnerability.Criteria, 0), } } // ApplyCriteria processes all criteria using type-switch dispatch to individual handlers func (b *searchQueryBuilder) ApplyCriteria(criteriaSet []vulnerability.Criteria) error { for _, c := range criteriaSet { applied := false switch c := c.(type) { case *search.PackageNameCriteria: b.handlePackageName(c) applied = true case *search.UnaffectedCriteria: b.handleUnaffected(c) applied = true case *search.EcosystemCriteria: b.handleEcosystem(c) applied = true case *search.IDCriteria: b.handleID(c) applied = true case *search.CPECriteria: if err := b.handleCPE(c); err != nil { return err } applied = true case *search.DistroCriteria: b.handleDistro(c) applied = true } if !applied { b.remainingCriteria = append(b.remainingCriteria, c) } } return nil } func (b *searchQueryBuilder) handlePackageName(c *search.PackageNameCriteria) { if b.query.pkgSpec == nil { b.query.pkgSpec = &PackageSpecifier{} } b.query.pkgSpec.Name = c.PackageName } func (b *searchQueryBuilder) handleUnaffected(_ *search.UnaffectedCriteria) { b.query.unaffectedOnly = true } func (b *searchQueryBuilder) handleEcosystem(c *search.EcosystemCriteria) { if b.query.pkgSpec == nil { b.query.pkgSpec = &PackageSpecifier{} } // the v6 store normalizes ecosystems around the syft package type, so that field is preferred switch { case c.PackageType != "" && c.PackageType != syftPkg.UnknownPkg: // prefer to match by a non-blank, known package type b.query.pkgType = c.PackageType b.query.pkgSpec.Ecosystem = string(c.PackageType) case c.Language != "": // if there's no known package type, but there is a non-blank language try that b.query.pkgSpec.Ecosystem = string(c.Language) case c.PackageType == syftPkg.UnknownPkg: // if language is blank, and package type is explicitly "UnknownPkg" and not just blank, use that b.query.pkgType = c.PackageType b.query.pkgSpec.Ecosystem = string(c.PackageType) } } func (b *searchQueryBuilder) handleID(c *search.IDCriteria) { b.query.vulnSpecs = append(b.query.vulnSpecs, VulnerabilitySpecifier{ Name: c.ID, }) } func (b *searchQueryBuilder) handleCPE(c *search.CPECriteria) error { if b.query.cpeSpec == nil { b.query.cpeSpec = &cpe.Attributes{} } *b.query.cpeSpec = c.CPE.Attributes if b.query.cpeSpec.Product == cpe.Any { return fmt.Errorf("must specify product to search by CPE; got: %s", c.CPE.Attributes.BindToFmtString()) } if b.query.pkgSpec == nil { b.query.pkgSpec = &PackageSpecifier{} } b.query.pkgSpec.CPE = &c.CPE.Attributes return nil } func (b *searchQueryBuilder) handleDistro(c *search.DistroCriteria) { for _, d := range c.Distros { var foundChannels int for _, channel := range d.Channels { if channel == "" { // if the channel is empty, we should not add it to the OS specifier continue } foundChannels++ b.query.osSpecs = append(b.query.osSpecs, &OSSpecifier{ Name: d.Name(), MajorVersion: d.MajorVersion(), MinorVersion: d.MinorVersion(), RemainingVersion: d.RemainingVersion(), LabelVersion: d.LabelVersion(), Channel: channel, DisableAliasing: c.Exact, }) } if foundChannels == 0 { b.query.osSpecs = append(b.query.osSpecs, &OSSpecifier{ Name: d.Name(), MajorVersion: d.MajorVersion(), MinorVersion: d.MinorVersion(), RemainingVersion: d.RemainingVersion(), LabelVersion: d.LabelVersion(), DisableAliasing: c.Exact, }) } } } // setDefaultOS sets default OS if none specified func (b *searchQueryBuilder) setDefaultOS() { if len(b.query.osSpecs) == 0 { // we don't want to search across all distros, instead if the user did not specify a distro we should assume that // they want to search across affected packages not associated with any distro. b.query.osSpecs = append(b.query.osSpecs, NoOSSpecified) } } // normalizePackageName normalizes package name if needed func (b *searchQueryBuilder) normalizePackageName() { if b.query.pkgType != "" && b.query.pkgSpec != nil && b.query.pkgSpec.Name != "" { b.query.pkgSpec.Name = name.Normalize(b.query.pkgSpec.Name, b.query.pkgType) } } // extractVersionMatcher extracts version constraints from remaining criteria func (b *searchQueryBuilder) extractVersionMatcher() { var remaining []vulnerability.Criteria var matcher search.VersionConstraintMatcher for _, c := range b.remainingCriteria { if nextMatcher, ok := c.(search.VersionConstraintMatcher); ok { if matcher == nil { matcher = nextMatcher } else { matcher = search.MultiConstraintMatcher(matcher, nextMatcher) } } else { remaining = append(remaining, c) } } b.query.versionMatcher = matcher b.remainingCriteria = remaining } // Build returns the final query and remaining criteria func (b *searchQueryBuilder) Build() (*searchQuery, []vulnerability.Criteria, error) { b.setDefaultOS() b.normalizePackageName() b.extractVersionMatcher() return b.query, b.remainingCriteria, nil } ================================================ FILE: grype/db/v6/search_query_test.go ================================================ package v6 import ( "testing" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestNewSearchCriteria(t *testing.T) { tests := []struct { name string criteria []vulnerability.Criteria validate func(t *testing.T, input *searchQuery) }{ { name: "package name criteria sets correct fields", criteria: []vulnerability.Criteria{ search.ByPackageName("test-package"), }, validate: func(t *testing.T, input *searchQuery) { require.NotNil(t, input.pkgSpec) require.Equal(t, "test-package", input.pkgSpec.Name) }, }, { name: "unaffected criteria sets flag", criteria: []vulnerability.Criteria{ search.ForUnaffected(), }, validate: func(t *testing.T, input *searchQuery) { require.True(t, input.unaffectedOnly) }, }, { name: "ecosystem criteria sets package type and ecosystem", criteria: []vulnerability.Criteria{ search.ByEcosystem(syftPkg.Java, syftPkg.JavaPkg), }, validate: func(t *testing.T, input *searchQuery) { require.NotNil(t, input.pkgSpec) require.Equal(t, syftPkg.JavaPkg, input.pkgType) require.Equal(t, "java-archive", input.pkgSpec.Ecosystem) }, }, { name: "ID criteria adds vulnerability spec", criteria: []vulnerability.Criteria{ search.ByID("CVE-2021-1234"), }, validate: func(t *testing.T, input *searchQuery) { require.Len(t, input.vulnSpecs, 1) require.Equal(t, "CVE-2021-1234", input.vulnSpecs[0].Name) }, }, { name: "distro criteria sets OS specs", criteria: []vulnerability.Criteria{ search.ByDistro(*distro.New(distro.Ubuntu, "20.04", "")), }, validate: func(t *testing.T, input *searchQuery) { require.Len(t, input.osSpecs, 1) require.Equal(t, "ubuntu", input.osSpecs[0].Name) require.Equal(t, "20", input.osSpecs[0].MajorVersion) }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { query, _, err := newSearchQuery(test.criteria) require.NoError(t, err) test.validate(t, query) }) } } func TestQueryBuilder_ApplyCriteria(t *testing.T) { tests := []struct { name string criteria []vulnerability.Criteria validate func(t *testing.T, builder *searchQueryBuilder) }{ { name: "package name criteria", criteria: []vulnerability.Criteria{ search.ByPackageName("test-package"), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.NotNil(t, builder.query.pkgSpec) require.Equal(t, "test-package", builder.query.pkgSpec.Name) }, }, { name: "unaffected criteria", criteria: []vulnerability.Criteria{ search.ForUnaffected(), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.True(t, builder.query.unaffectedOnly) }, }, { name: "ecosystem criteria with package type", criteria: []vulnerability.Criteria{ search.ByEcosystem(syftPkg.Java, syftPkg.JavaPkg), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.NotNil(t, builder.query.pkgSpec) require.Equal(t, syftPkg.JavaPkg, builder.query.pkgType) require.Equal(t, "java-archive", builder.query.pkgSpec.Ecosystem) }, }, { name: "ID criteria", criteria: []vulnerability.Criteria{ search.ByID("CVE-2021-1234"), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.Len(t, builder.query.vulnSpecs, 1) require.Equal(t, "CVE-2021-1234", builder.query.vulnSpecs[0].Name) }, }, { name: "CPE criteria", criteria: []vulnerability.Criteria{ search.ByCPE(cpe.Must("cpe:2.3:a:apache:tomcat:9.0.0:*:*:*:*:*:*:*", "")), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.NotNil(t, builder.query.cpeSpec) require.Equal(t, "apache", builder.query.cpeSpec.Vendor) require.Equal(t, "tomcat", builder.query.cpeSpec.Product) require.NotNil(t, builder.query.pkgSpec) require.NotNil(t, builder.query.pkgSpec.CPE) }, }, { name: "distro criteria", criteria: []vulnerability.Criteria{ search.ByDistro(*distro.New(distro.Ubuntu, "20.04", "")), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.Len(t, builder.query.osSpecs, 1) require.Equal(t, "ubuntu", builder.query.osSpecs[0].Name) require.Equal(t, "20", builder.query.osSpecs[0].MajorVersion) }, }, { name: "multiple criteria", criteria: []vulnerability.Criteria{ search.ByPackageName("test-package"), search.ForUnaffected(), search.ByID("CVE-2021-1234"), }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.NotNil(t, builder.query.pkgSpec) require.Equal(t, "test-package", builder.query.pkgSpec.Name) require.True(t, builder.query.unaffectedOnly) require.Len(t, builder.query.vulnSpecs, 1) require.Equal(t, "CVE-2021-1234", builder.query.vulnSpecs[0].Name) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { builder := newSearchQueryBuilder() err := builder.ApplyCriteria(tt.criteria) require.NoError(t, err) tt.validate(t, builder) }) } } func TestQueryBuilder_CPEErrorHandling(t *testing.T) { builder := newSearchQueryBuilder() // create a CPE without a product (which should cause an error) invalidCPE := cpe.CPE{ Attributes: cpe.Attributes{ Part: "a", Vendor: "vendor", // no product specified }, } criteria := []vulnerability.Criteria{ search.ByCPE(invalidCPE), } err := builder.ApplyCriteria(criteria) require.Error(t, err) require.Contains(t, err.Error(), "must specify product to search by CPE") } func TestQueryBuilder_PostProcess(t *testing.T) { tests := []struct { name string setup func(*searchQueryBuilder) validate func(t *testing.T, builder *searchQueryBuilder) }{ { name: "sets default OS when none specified", setup: func(builder *searchQueryBuilder) { // no OS specs set }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.Len(t, builder.query.osSpecs, 1) require.Equal(t, NoOSSpecified, builder.query.osSpecs[0]) }, }, { name: "does not override existing OS specs", setup: func(builder *searchQueryBuilder) { builder.query.osSpecs = append(builder.query.osSpecs, &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", }) }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.Len(t, builder.query.osSpecs, 1) require.Equal(t, "ubuntu", builder.query.osSpecs[0].Name) }, }, { name: "normalizes package name when pkgType and pkgSpec are set", setup: func(builder *searchQueryBuilder) { builder.query.pkgType = syftPkg.GemPkg builder.query.pkgSpec = &PackageSpecifier{ Name: "Test_Package", } }, validate: func(t *testing.T, builder *searchQueryBuilder) { // verify that normalization was attempted (actual result may vary by package type) require.NotEmpty(t, builder.query.pkgSpec.Name) }, }, { name: "preserves remaining criteria that aren't processed", setup: func(builder *searchQueryBuilder) { // add some criteria that should remain builder.remainingCriteria = []vulnerability.Criteria{ search.ByFunc(func(vulnerability.Vulnerability) (bool, string, error) { return true, "", nil }), } }, validate: func(t *testing.T, builder *searchQueryBuilder) { require.Len(t, builder.remainingCriteria, 1, "func criteria should remain unprocessed") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { builder := newSearchQueryBuilder() tt.setup(builder) _, _, err := builder.Build() require.NoError(t, err) tt.validate(t, builder) }) } } func TestQueryBuilder_Build(t *testing.T) { builder := newSearchQueryBuilder() // add some test data builder.query.unaffectedOnly = true builder.remainingCriteria = []vulnerability.Criteria{ search.ByPackageName("some-remaining-criteria"), } query, remaining, err := builder.Build() require.NoError(t, err) require.NotNil(t, query) require.True(t, query.unaffectedOnly) require.Len(t, remaining, 1) } func TestQueryBuilder_ExactDistroCriteria(t *testing.T) { tests := []struct { name string criteria []vulnerability.Criteria validate func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) }{ { name: "exact distro criteria should be handled with DisableAliasing set", criteria: []vulnerability.Criteria{ search.ByExactDistro(*distro.New(distro.AlmaLinux, "8", "")), }, validate: func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) { require.Len(t, query.osSpecs, 1) require.Equal(t, "almalinux", query.osSpecs[0].Name) require.Equal(t, "8", query.osSpecs[0].MajorVersion) require.True(t, query.osSpecs[0].DisableAliasing, "ExactDistroCriteria should set DisableAliasing=true") require.Empty(t, remaining, "ExactDistroCriteria should be handled, not left in remaining") }, }, { name: "exact distro criteria should not be left in remaining criteria", criteria: []vulnerability.Criteria{ search.ByPackageName("mariadb"), search.ByExactDistro(*distro.New(distro.AlmaLinux, "8", "")), search.ForUnaffected(), }, validate: func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) { require.NotNil(t, query.pkgSpec) require.Equal(t, "mariadb", query.pkgSpec.Name) require.Len(t, query.osSpecs, 1) require.Equal(t, "almalinux", query.osSpecs[0].Name) require.True(t, query.osSpecs[0].DisableAliasing, "ExactDistroCriteria should set DisableAliasing=true") require.True(t, query.unaffectedOnly) require.Empty(t, remaining, "ExactDistroCriteria should be handled, not left in remaining") }, }, { name: "regular distro criteria should not set DisableAliasing", criteria: []vulnerability.Criteria{ search.ByDistro(*distro.New(distro.AlmaLinux, "8", "")), }, validate: func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) { require.Len(t, query.osSpecs, 1) require.Equal(t, "almalinux", query.osSpecs[0].Name) require.Equal(t, "8", query.osSpecs[0].MajorVersion) require.False(t, query.osSpecs[0].DisableAliasing, "Regular DistroCriteria should keep DisableAliasing=false") require.Empty(t, remaining) }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { query, remaining, err := newSearchQuery(test.criteria) require.NoError(t, err) test.validate(t, query, remaining) }) } } func TestQueryBuilder_IntegrationWithRealCriteria(t *testing.T) { // test the full flow that mimics parseCriteria behavior criteria := []vulnerability.Criteria{ search.ByPackageName("log4j"), search.ByEcosystem(syftPkg.Java, syftPkg.JavaPkg), search.ByDistro(*distro.New(distro.Ubuntu, "20.04", "")), search.ByID("CVE-2021-44228"), search.ForUnaffected(), search.ByFunc(func(vulnerability.Vulnerability) (bool, string, error) { return true, "", nil }), } builder := newSearchQueryBuilder() err := builder.ApplyCriteria(criteria) require.NoError(t, err) query, remaining, err := builder.Build() require.NoError(t, err) // validate the built query require.NotNil(t, query.pkgSpec) require.Equal(t, "log4j", query.pkgSpec.Name) require.Equal(t, syftPkg.JavaPkg, query.pkgType) require.Len(t, query.osSpecs, 1) require.Equal(t, "ubuntu", query.osSpecs[0].Name) require.Len(t, query.vulnSpecs, 1) require.Equal(t, "CVE-2021-44228", query.vulnSpecs[0].Name) require.True(t, query.unaffectedOnly) // func criteria should remain unprocessed require.Len(t, remaining, 1) } ================================================ FILE: grype/db/v6/severity.go ================================================ package v6 import ( "fmt" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/cvss" "github.com/anchore/grype/internal/log" ) func extractSeverities(vuln *VulnerabilityHandle) (vulnerability.Severity, []vulnerability.Cvss, error) { if vuln.BlobValue == nil { return vulnerability.UnknownSeverity, nil, nil } sev := vulnerability.UnknownSeverity if len(vuln.BlobValue.Severities) > 0 { var err error // grype DB v6+ will order the set of severities by rank, so we can just take the first one sev, err = extractSeverity(vuln.BlobValue.Severities[0].Value) if err != nil { return vulnerability.UnknownSeverity, nil, fmt.Errorf("unable to extract severity: %w", err) } } return sev, toCvss(vuln.BlobValue.Severities...), nil } func extractSeverity(severity any) (vulnerability.Severity, error) { switch sev := severity.(type) { case string: return vulnerability.ParseSeverity(sev), nil case CVSSSeverity: metrics, err := cvss.ParseMetricsFromVector(sev.Vector) if err != nil { return vulnerability.UnknownSeverity, fmt.Errorf("unable to parse CVSS vector: %w", err) } if metrics == nil { return vulnerability.UnknownSeverity, nil } return interpretCVSS(metrics.BaseScore, sev.Version), nil default: return vulnerability.UnknownSeverity, nil } } func interpretCVSS(score float64, version string) vulnerability.Severity { switch version { case "2.0": return interpretCVSSv2(score) case "3.0", "3.1", "4.0": return interpretCVSSv3Plus(score) default: return vulnerability.UnknownSeverity } } func interpretCVSSv2(score float64) vulnerability.Severity { if score < 0 { return vulnerability.UnknownSeverity } if score == 0 { return vulnerability.NegligibleSeverity } if score < 4.0 { return vulnerability.LowSeverity } if score < 7.0 { return vulnerability.MediumSeverity } if score <= 10.0 { return vulnerability.HighSeverity } return vulnerability.UnknownSeverity } func interpretCVSSv3Plus(score float64) vulnerability.Severity { if score < 0 { return vulnerability.UnknownSeverity } if score == 0 { return vulnerability.NegligibleSeverity } if score < 4.0 { return vulnerability.LowSeverity } if score < 7.0 { return vulnerability.MediumSeverity } if score < 9.0 { return vulnerability.HighSeverity } if score <= 10.0 { return vulnerability.CriticalSeverity } return vulnerability.UnknownSeverity } func toCvss(severities ...Severity) []vulnerability.Cvss { //nolint:prealloc var out []vulnerability.Cvss for _, sev := range severities { switch sev.Scheme { case SeveritySchemeCVSS: default: // not a CVSS score continue } cvssSev, ok := sev.Value.(CVSSSeverity) if !ok { // not a CVSS score continue } var usedMetrics vulnerability.CvssMetrics // though the DB has the base score, we parse the vector for all metrics metrics, err := cvss.ParseMetricsFromVector(cvssSev.Vector) if err != nil { log.WithFields("vector", cvssSev.Vector, "error", err).Warn("unable to parse CVSS vector") continue } if metrics != nil { usedMetrics = *metrics } out = append(out, vulnerability.Cvss{ Source: sev.Source, Type: legacyCVSSType(sev.Rank), Version: cvssSev.Version, Vector: cvssSev.Vector, Metrics: usedMetrics, }) } return out } func legacyCVSSType(rank int) string { if rank == 1 { return "Primary" } return "Secondary" } ================================================ FILE: grype/db/v6/severity_test.go ================================================ package v6 import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func TestExtractSeverity(t *testing.T) { tests := []struct { name string input any expected vulnerability.Severity expectedErr require.ErrorAssertionFunc }{ { name: "string low severity", input: "low", expected: vulnerability.LowSeverity, expectedErr: require.NoError, }, { name: "string high severity", input: "high", expected: vulnerability.HighSeverity, expectedErr: require.NoError, }, { name: "string critical severity", input: "critical", expected: vulnerability.CriticalSeverity, expectedErr: require.NoError, }, { name: "string unknown severity", input: "invalid", expected: vulnerability.UnknownSeverity, expectedErr: require.NoError, }, { name: "CVSS v2 low severity", input: CVSSSeverity{ Version: "2.0", Vector: "AV:L/AC:L/Au:N/C:N/I:P/A:N", }, expected: vulnerability.LowSeverity, expectedErr: require.NoError, }, { name: "CVSS v2 medium severity", input: CVSSSeverity{ Version: "2.0", Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:N", }, expected: vulnerability.MediumSeverity, expectedErr: require.NoError, }, { name: "CVSS v2 high severity", input: CVSSSeverity{ Version: "2.0", Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", }, expected: vulnerability.HighSeverity, expectedErr: require.NoError, }, { name: "CVSS v3 negligible severity", input: CVSSSeverity{ Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N", }, expected: vulnerability.NegligibleSeverity, expectedErr: require.NoError, }, { name: "CVSS v3 critical severity", input: CVSSSeverity{ Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", }, expected: vulnerability.CriticalSeverity, expectedErr: require.NoError, }, { name: "CVSS v4 critical severity", input: CVSSSeverity{ Version: "4.0", Vector: "CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:H/VA:L/SC:L/SI:H/SA:L/MAC:L/MAT:P/MPR:N/S:N/R:A/RE:L/U:Clear", }, expected: vulnerability.CriticalSeverity, expectedErr: require.NoError, }, { name: "invalid CVSS vector", input: CVSSSeverity{ Version: "3.1", Vector: "INVALID", }, expected: vulnerability.UnknownSeverity, expectedErr: require.Error, }, { name: "invalid type", input: 123, expected: vulnerability.UnknownSeverity, expectedErr: require.NoError, }, { name: "nil input", input: nil, expected: vulnerability.UnknownSeverity, expectedErr: require.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := extractSeverity(tt.input) tt.expectedErr(t, err) assert.Equal(t, tt.expected, result) }) } } func TestExtractSeverities(t *testing.T) { tests := []struct { name string input *VulnerabilityHandle expectedSev vulnerability.Severity expectedCVSS []vulnerability.Cvss expectedError require.ErrorAssertionFunc }{ { name: "nil blob", input: &VulnerabilityHandle{BlobValue: nil}, expectedSev: vulnerability.UnknownSeverity, expectedCVSS: nil, expectedError: require.NoError, }, { name: "empty severities", input: &VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ Severities: []Severity{}, }, }, expectedSev: vulnerability.UnknownSeverity, expectedCVSS: nil, expectedError: require.NoError, }, { name: "valid primary CVSS severity", input: &VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ Severities: []Severity{ { Scheme: SeveritySchemeCVSS, Source: "NVD", Value: CVSSSeverity{ Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", }, Rank: 1, }, }, }, }, expectedSev: vulnerability.CriticalSeverity, expectedCVSS: []vulnerability.Cvss{ { Source: "NVD", Type: "Primary", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 9.8, ExploitabilityScore: ptr(3.9), ImpactScore: ptr(5.9), }, }, }, expectedError: require.NoError, }, { name: "valid secondary CVSS severity", input: &VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ Severities: []Severity{ { Scheme: SeveritySchemeCVSS, Source: "NVD", Value: CVSSSeverity{ Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", }, Rank: 2, }, }, }, }, expectedSev: vulnerability.CriticalSeverity, expectedCVSS: []vulnerability.Cvss{ { Source: "NVD", Type: "Secondary", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 9.8, ExploitabilityScore: ptr(3.9), ImpactScore: ptr(5.9), }, }, }, expectedError: require.NoError, }, { name: "valid CVSS severity with unknown rank (default to secondary)", input: &VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ Severities: []Severity{ { Scheme: SeveritySchemeCVSS, Source: "NVD", Value: CVSSSeverity{ Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", }, Rank: 3, }, }, }, }, expectedSev: vulnerability.CriticalSeverity, expectedCVSS: []vulnerability.Cvss{ { Source: "NVD", Type: "Secondary", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 9.8, ExploitabilityScore: ptr(3.9), ImpactScore: ptr(5.9), }, }, }, expectedError: require.NoError, }, { name: "invalid CVSS vector", input: &VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ Severities: []Severity{ { Scheme: SeveritySchemeCVSS, Value: CVSSSeverity{ Version: "3.1", Vector: "INVALID", }, }, }, }, }, expectedSev: vulnerability.UnknownSeverity, expectedCVSS: nil, expectedError: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectedError == nil { tt.expectedError = require.NoError } sev, cvss, err := extractSeverities(tt.input) tt.expectedError(t, err) assert.Equal(t, tt.expectedSev, sev) assert.Equal(t, tt.expectedCVSS, cvss) }) } } ================================================ FILE: grype/db/v6/store.go ================================================ package v6 import ( "fmt" "strings" "gorm.io/gorm" "github.com/anchore/grype/internal/log" ) type store struct { *dbMetadataStore *providerStore *vulnerabilityStore *operatingSystemStore *affectedPackageStore *unaffectedPackageStore *affectedCPEStore *unaffectedCPEStore *vulnerabilityDecoratorStore blobStore *blobStore db *gorm.DB config Config empty bool writable bool } func (s *store) GetDB() *gorm.DB { return s.db } func (s *store) attachBlobValue(values ...blobable) error { return s.blobStore.attachBlobValue(values...) } func InitialData() []any { var data []any os := KnownOperatingSystemSpecifierOverrides() for i := range os { data = append(data, &os[i]) } p := KnownPackageSpecifierOverrides() for i := range p { data = append(data, &p[i]) } return data } func newStore(cfg Config, empty, writable bool) (*store, error) { var path string if cfg.DBDirPath != "" { path = cfg.DBFilePath() } db, err := NewLowLevelDB(path, empty, writable, cfg.Debug) if err != nil { return nil, fmt.Errorf("failed to open db: %w", err) } metadataStore := newDBMetadataStore(db) if empty { if err := metadataStore.SetDBMetadata(); err != nil { return nil, fmt.Errorf("failed to set db metadata: %w", err) } } meta, err := metadataStore.GetDBMetadata() if err != nil || meta == nil || meta.Model != ModelVersion { // db.Close must be called, or we will get stale reads d, _ := db.DB() if d != nil { _ = d.Close() } if err != nil { return nil, fmt.Errorf("not a v%d database: %w", ModelVersion, err) } return nil, fmt.Errorf("not a v%d database", ModelVersion) } dbVersion := newSchemaVerFromDBMetadata(*meta) bs := newBlobStore(db) osStore := newOperatingSystemStore(db, bs) return &store{ dbMetadataStore: metadataStore, providerStore: newProviderStore(db), vulnerabilityStore: newVulnerabilityStore(db, bs), operatingSystemStore: osStore, affectedPackageStore: newAffectedPackageStore(db, bs, osStore), unaffectedPackageStore: newUnaffectedPackageStore(db, bs, osStore), affectedCPEStore: newAffectedCPEStore(db, bs), vulnerabilityDecoratorStore: newVulnerabilityDecoratorStore(db, bs, dbVersion), unaffectedCPEStore: newUnaffectedCPEStore(db, bs), blobStore: bs, db: db, config: cfg, empty: empty, writable: writable, }, nil } // Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, only closes the connection to the DB. func (s *store) Close() error { if !s.writable || !s.empty { d, err := s.db.DB() if err == nil { return d.Close() } // if not empty, this writable execution created indexes return nil } log.Debug("closing store") // drop all indexes, which saves a lot of space distribution-wise (these get re-created on running gorm auto-migrate) if err := dropAllIndexes(s.db); err != nil { return err } // compact the DB size log.Debug("vacuuming database") if err := s.db.Exec("VACUUM").Error; err != nil { return fmt.Errorf("failed to vacuum: %w", err) } // since we are using riskier statements to optimize write speeds, do a last integrity check log.Debug("running integrity check") if err := s.db.Exec("PRAGMA integrity_check").Error; err != nil { return fmt.Errorf("integrity check failed: %w", err) } d, err := s.db.DB() if err != nil { return err } return d.Close() } func dropAllIndexes(db *gorm.DB) error { tables, err := db.Migrator().GetTables() if err != nil { return fmt.Errorf("failed to get tables: %w", err) } log.WithFields("tables", len(tables)).Debug("discovering indexes") for _, table := range tables { indexes, err := db.Migrator().GetIndexes(table) if err != nil { return fmt.Errorf("failed to get indexes for table %s: %w", table, err) } log.WithFields("table", table, "indexes", len(indexes)).Trace("dropping indexes") for _, index := range indexes { // skip auto-generated UNIQUE or PRIMARY KEY indexes (sqlite will not allow you to drop these without more major surgery) if strings.HasPrefix(index.Name(), "sqlite_autoindex") { log.WithFields("table", table, "index", index.Name()).Trace("skip dropping autoindex") continue } log.WithFields("table", table, "index", index.Name()).Trace("dropping index") if err := db.Migrator().DropIndex(table, index.Name()); err != nil { return fmt.Errorf("failed to drop index %s on table %s: %w", index, table, err) } } } return nil } ================================================ FILE: grype/db/v6/store_test.go ================================================ package v6 import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestStoreClose(t *testing.T) { t.Run("readonly mode does nothing", func(t *testing.T) { dir := t.TempDir() s := setupTestStore(t, dir) s.empty = false s.writable = false err := s.Close() require.NoError(t, err) // ensure the connection is no longer open var indexes []string s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes) assert.Empty(t, indexes) // get a new connection (readonly) s = setupReadOnlyTestStore(t, dir) // ensure we have our indexes indexes = nil s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes) assert.NotEmpty(t, indexes) }) t.Run("successful close in writable mode", func(t *testing.T) { dir := t.TempDir() s := setupTestStore(t, dir) // ensure we have indexes to start with var indexes []string s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes) assert.NotEmpty(t, indexes) err := s.Close() require.NoError(t, err) // get a new connection (readonly) s = setupReadOnlyTestStore(t, dir) // ensure all of our indexes were dropped indexes = nil s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes) assert.Empty(t, indexes) }) } func Test_oldDbV5(t *testing.T) { s := setupTestStore(t) require.NoError(t, s.db.Where("true").Delete(&DBMetadata{}).Error) // delete all existing records require.NoError(t, s.Close()) s, err := newStore(s.config, false, true) require.Nil(t, s) require.ErrorIs(t, err, gorm.ErrRecordNotFound) require.ErrorContains(t, err, fmt.Sprintf("not a v%d database", ModelVersion)) } func Test_oldDbWithMetadata(t *testing.T) { s := setupTestStore(t) require.NoError(t, s.db.Where("true").Model(DBMetadata{}).Update("Model", "5").Error) // old database version require.NoError(t, s.Close()) s, err := newStore(s.config, false, true) require.Nil(t, s) require.NotErrorIs(t, err, gorm.ErrRecordNotFound) require.ErrorContains(t, err, fmt.Sprintf("not a v%d database", ModelVersion)) } ================================================ FILE: grype/db/v6/unaffected_cpe_store.go ================================================ package v6 import ( "gorm.io/gorm" "github.com/anchore/syft/syft/cpe" ) type UnaffectedCPEStoreWriter interface { AddUnaffectedCPEs(packages ...*UnaffectedCPEHandle) error } type UnaffectedCPEStoreReader interface { GetUnaffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]UnaffectedCPEHandle, error) } type unaffectedCPEStore struct { db *gorm.DB blobStore *blobStore cpeStore *cpeStore } func newUnaffectedCPEStore(db *gorm.DB, bs *blobStore) *unaffectedCPEStore { return &unaffectedCPEStore{ db: db, blobStore: bs, cpeStore: newCPEStore(db, bs), } } func (s *unaffectedCPEStore) AddUnaffectedCPEs(packages ...*UnaffectedCPEHandle) error { return addCPEHandles(s.cpeStore, packages...) } func (s *unaffectedCPEStore) GetUnaffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]UnaffectedCPEHandle, error) { results, err := getCPEHandles[*UnaffectedCPEHandle]( s.cpeStore, cpe, config, "unaffected_cpe_handles", ) if err != nil { return nil, err } models := make([]UnaffectedCPEHandle, len(results)) for i, r := range results { models[i] = *r } return models, nil } ================================================ FILE: grype/db/v6/unaffected_cpe_store_test.go ================================================ package v6 import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUnaffectedCPEStore_AddUnaffectedCPEs(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newUnaffectedCPEStore(db, bw) cpe1 := &UnaffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ // vuln id = 1 Provider: &Provider{ ID: "nvd", }, Name: "CVE-2023-5678", }, CPE: &Cpe{ Part: "a", Vendor: "vendor-1", Product: "product-1", Edition: "edition-1", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } cpe2 := testUnaffectedCPEHandle() // vuln id = 2 err := s.AddUnaffectedCPEs(cpe1, cpe2) require.NoError(t, err) var result1 UnaffectedCPEHandle err = db.Where("cpe_id = ?", 1).First(&result1).Error require.NoError(t, err) assert.Equal(t, cpe1.VulnerabilityID, result1.VulnerabilityID) assert.Equal(t, cpe1.ID, result1.ID) assert.Equal(t, cpe1.BlobID, result1.BlobID) assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch var result2 UnaffectedCPEHandle err = db.Where("cpe_id = ?", 2).First(&result2).Error require.NoError(t, err) assert.Equal(t, cpe2.VulnerabilityID, result2.VulnerabilityID) assert.Equal(t, cpe2.ID, result2.ID) assert.Equal(t, cpe2.BlobID, result2.BlobID) assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch } func TestUnaffectedCPEStore_GetCPEs(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newUnaffectedCPEStore(db, bw) c := testUnaffectedCPEHandle() err := s.AddUnaffectedCPEs(c) require.NoError(t, err) results, err := s.GetUnaffectedCPEs(cpeFromProduct(c.CPE.Product), nil) require.NoError(t, err) expected := []UnaffectedCPEHandle{*c} require.Len(t, results, len(expected)) result := results[0] assert.Equal(t, c.CpeID, result.CpeID) assert.Equal(t, c.ID, result.ID) assert.Equal(t, c.BlobID, result.BlobID) require.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch // fetch again with blob & cpe preloaded results, err = s.GetUnaffectedCPEs(cpeFromProduct(c.CPE.Product), &GetCPEOptions{PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true}) require.NoError(t, err) require.Len(t, results, len(expected)) result = results[0] assert.NotNil(t, result.BlobValue) if d := cmp.Diff(*c, result); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } func TestUnaffectedCPEStore_GetExact(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newUnaffectedCPEStore(db, bw) c := testUnaffectedCPEHandle() err := s.AddUnaffectedCPEs(c) require.NoError(t, err) // we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses) results, err := s.GetUnaffectedCPEs(toCPE(c.CPE), nil) require.NoError(t, err) expected := []UnaffectedCPEHandle{*c} require.Len(t, results, len(expected)) result := results[0] assert.Equal(t, c.CpeID, result.CpeID) } func TestUnaffectedCPEStore_Get_CaseInsensitive(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newUnaffectedCPEStore(db, bw) c := testUnaffectedCPEHandle() err := s.AddUnaffectedCPEs(c) require.NoError(t, err) // we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses) results, err := s.GetUnaffectedCPEs(toCPE(&Cpe{ Part: "Application", // capitalized Vendor: "Vendor", // capitalized Product: "Product", // capitalized Edition: "Edition", // capitalized Language: "Language", // capitalized SoftwareEdition: "Software_edition", // capitalized TargetHardware: "Target_hardware", // capitalized TargetSoftware: "Target_software", // capitalized Other: "Other", // capitalized }), nil) require.NoError(t, err) expected := []UnaffectedCPEHandle{*c} require.Len(t, results, len(expected)) result := results[0] assert.Equal(t, c.CpeID, result.CpeID) } func TestUnaffectedCPEStore_PreventDuplicateCPEs(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newUnaffectedCPEStore(db, bw) cpe1 := &UnaffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ // vuln id = 1 Name: "CVE-2023-5678", Provider: &Provider{ ID: "nvd", }, }, CPE: &Cpe{ // ID = 1 Part: "a", Vendor: "vendor-1", Product: "product-1", Edition: "edition-1", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } err := s.AddUnaffectedCPEs(cpe1) require.NoError(t, err) // attempt to add a duplicate CPE with the same values duplicateCPE := &UnaffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ // vuln id = 2, different VulnerabilityID for testing... Name: "CVE-2024-1234", Provider: &Provider{ ID: "nvd", }, }, CpeID: 2, // for testing explicitly set to 2, but this is unrealistic CPE: &Cpe{ ID: 2, // different, again, unrealistic but useful for testing Part: "a", // same Vendor: "vendor-1", // same Product: "product-1", // same Edition: "edition-1", // same }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2024-1234"}, }, } err = s.AddUnaffectedCPEs(duplicateCPE) require.NoError(t, err) require.Equal(t, cpe1.CpeID, duplicateCPE.CpeID, "expected the CPE DB ID to be the same") var existingCPEs []Cpe err = db.Find(&existingCPEs).Error require.NoError(t, err) require.Len(t, existingCPEs, 1, "expected only one CPE to exist") actualHandles, err := s.GetUnaffectedCPEs(cpeFromProduct(cpe1.CPE.Product), &GetCPEOptions{ PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true, }) require.NoError(t, err) // the CPEs should be the same, and the store should reconcile the IDs duplicateCPE.CpeID = cpe1.CpeID duplicateCPE.CPE.ID = cpe1.CPE.ID expected := []UnaffectedCPEHandle{*cpe1, *duplicateCPE} require.Len(t, actualHandles, len(expected), "expected both handles to be stored") if d := cmp.Diff(expected, actualHandles); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } func testUnaffectedCPEHandle() *UnaffectedCPEHandle { return &UnaffectedCPEHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2024-4321", Provider: &Provider{ ID: "nvd", }, }, CPE: &Cpe{ Part: "application", Vendor: "vendor", Product: "product", Edition: "edition", Language: "language", SoftwareEdition: "software_edition", TargetHardware: "target_hardware", TargetSoftware: "target_software", Other: "other", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2024-4321"}, }, } } ================================================ FILE: grype/db/v6/unaffected_package_store.go ================================================ package v6 import "gorm.io/gorm" type UnaffectedPackageStoreWriter interface { AddUnaffectedPackages(packages ...*UnaffectedPackageHandle) error } type UnaffectedPackageStoreReader interface { GetUnaffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]UnaffectedPackageHandle, error) } type unaffectedPackageStore struct { db *gorm.DB osStore *operatingSystemStore pkgStore *packageStore } func newUnaffectedPackageStore(db *gorm.DB, bs *blobStore, oss *operatingSystemStore) *unaffectedPackageStore { return &unaffectedPackageStore{ db: db, osStore: oss, pkgStore: newPackageStore(db, bs, oss), } } func (s *unaffectedPackageStore) AddUnaffectedPackages(packages ...*UnaffectedPackageHandle) error { return addPackagesWithOS(s.pkgStore, packages...) } func (s *unaffectedPackageStore) GetUnaffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]UnaffectedPackageHandle, error) { results, err := getPackages[*UnaffectedPackageHandle]( s.pkgStore, pkg, config, "unaffected_package_handles", ) if err != nil { return nil, err } models := make([]UnaffectedPackageHandle, len(results)) for i, r := range results { models[i] = *r } return models, nil } ================================================ FILE: grype/db/v6/unaffected_package_store_test.go ================================================ package v6 import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" ) type unaffectedPackageHandlePreloadConfig struct { name string PreloadOS bool PreloadPackage bool PreloadBlob bool PreloadVulnerability bool prepExpectations func(*testing.T, []UnaffectedPackageHandle) []UnaffectedPackageHandle } func defaultUnaffectedPackageHandlePreloadCases() []unaffectedPackageHandlePreloadConfig { return []unaffectedPackageHandlePreloadConfig{ { name: "preload-all", PreloadOS: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, prepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle { for _, a := range in { if a.OperatingSystemID != nil { require.NotNil(t, a.OperatingSystem) } require.NotNil(t, a.Package) require.NotNil(t, a.BlobValue) require.NotNil(t, a.Vulnerability) } return in }, }, { name: "preload-none", prepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle { var out []UnaffectedPackageHandle for _, a := range in { if a.OperatingSystem == nil && a.BlobValue == nil && a.Package == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.Package = nil a.BlobValue = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-os-only", PreloadOS: true, prepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle { var out []UnaffectedPackageHandle for _, a := range in { if a.OperatingSystemID != nil { require.NotNil(t, a.OperatingSystem) } if a.Package == nil && a.BlobValue == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.Package = nil a.BlobValue = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-package-only", PreloadPackage: true, prepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle { var out []UnaffectedPackageHandle for _, a := range in { require.NotNil(t, a.Package) if a.OperatingSystem == nil && a.BlobValue == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.BlobValue = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-blob-only", PreloadBlob: true, prepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle { var out []UnaffectedPackageHandle for _, a := range in { if a.OperatingSystem == nil && a.Package == nil && a.Vulnerability == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.Package = nil a.Vulnerability = nil out = append(out, a) } return out }, }, { name: "preload-vulnerability-only", PreloadVulnerability: true, prepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle { var out []UnaffectedPackageHandle for _, a := range in { if a.OperatingSystem == nil && a.Package == nil && a.BlobValue == nil { t.Skip("preload already matches expectation") } a.OperatingSystem = nil a.Package = nil a.BlobValue = nil out = append(out, a) } return out }, }, } } func TestUnaffectedPackageStore_AddUnaffectedPackages(t *testing.T) { setupUnaffectedPackageStore := func(t *testing.T) *unaffectedPackageStore { db := setupTestStore(t).db bs := newBlobStore(db) return newUnaffectedPackageStore(db, bs, newOperatingSystemStore(db, bs)) } setupTestStoreWithPackages := func(t *testing.T) (*UnaffectedPackageHandle, *UnaffectedPackageHandle, *unaffectedPackageStore) { pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1"}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := testDistro1UnaffectedPackage2Handle() return pkg1, pkg2, setupUnaffectedPackageStore(t) } t.Run("no preloading", func(t *testing.T) { pkg1, pkg2, s := setupTestStoreWithPackages(t) err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) var result1 UnaffectedPackageHandle err = s.db.Where("package_id = ?", pkg1.PackageID).First(&result1).Error require.NoError(t, err) assert.Equal(t, pkg1.PackageID, result1.PackageID) assert.Equal(t, pkg1.BlobID, result1.BlobID) require.Nil(t, result1.BlobValue) // no preloading on fetch var result2 UnaffectedPackageHandle err = s.db.Where("package_id = ?", pkg2.PackageID).First(&result2).Error require.NoError(t, err) assert.Equal(t, pkg2.PackageID, result2.PackageID) assert.Equal(t, pkg2.BlobID, result2.BlobID) require.Nil(t, result2.BlobValue) }) t.Run("preloading", func(t *testing.T) { pkg1, pkg2, s := setupTestStoreWithPackages(t) err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) options := &GetPackageOptions{ PreloadOS: true, PreloadPackage: true, PreloadBlob: true, } results, err := s.GetUnaffectedPackages(pkgFromName(pkg1.Package.Name), options) require.NoError(t, err) require.Len(t, results, 1) result := results[0] require.NotNil(t, result.Package) require.NotNil(t, result.BlobValue) assert.Nil(t, result.OperatingSystem) // pkg1 has no OS }) t.Run("preload CPEs", func(t *testing.T) { pkg1, _, s := setupTestStoreWithPackages(t) c := Cpe{ Part: "a", Vendor: "vendor1", Product: "product1", } pkg1.Package.CPEs = []Cpe{c} err := s.AddUnaffectedPackages(pkg1) require.NoError(t, err) options := &GetPackageOptions{ PreloadPackage: true, PreloadPackageCPEs: true, } results, err := s.GetUnaffectedPackages(pkgFromName(pkg1.Package.Name), options) require.NoError(t, err) require.Len(t, results, 1) result := results[0] require.NotNil(t, result.Package) // the IDs should have been set, and there is only one, so we know the correct values c.ID = 1 if d := cmp.Diff([]Cpe{c}, result.Package.CPEs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) t.Run("Package deduplication", func(t *testing.T) { pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1"}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1"}, // same! BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-56789"}, }, } s := setupUnaffectedPackageStore(t) err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) var pkgs []Package err = s.db.Find(&pkgs).Error require.NoError(t, err) expected := []Package{ *pkg1.Package, } if d := cmp.Diff(expected, pkgs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) t.Run("same package with multiple CPEs", func(t *testing.T) { cpe1 := Cpe{ Part: "a", Vendor: "vendor1", Product: "product1", } cpe2 := Cpe{ Part: "a", Vendor: "vendor2", Product: "product2", } pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-56789", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1, cpe2}}, // duplicate CPE + additional CPE BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-56789"}, }, } s := setupUnaffectedPackageStore(t) err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) var pkgs []Package err = s.db.Preload("CPEs").Find(&pkgs).Error require.NoError(t, err) expPkg := *pkg1.Package expPkg.ID = 1 cpe1.ID = 1 cpe2.ID = 2 expPkg.CPEs = []Cpe{cpe1, cpe2} expected := []Package{ expPkg, } if d := cmp.Diff(expected, pkgs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } expectedCPEs := []Cpe{cpe1, cpe2} var cpeResults []Cpe err = s.db.Find(&cpeResults).Error require.NoError(t, err) if d := cmp.Diff(expectedCPEs, cpeResults); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) t.Run("allow same CPE to belong to multiple packages", func(t *testing.T) { cpe1 := Cpe{ Part: "a", Vendor: "vendor1", Product: "product1", } cpe2 := Cpe{ Part: "a", Vendor: "vendor2", Product: "product2", } pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-56789", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg2", Ecosystem: "type1", CPEs: []Cpe{cpe1, cpe2}}, // overlapping CPEs for different packages BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-56789"}, }, } s := setupUnaffectedPackageStore(t) err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) var pkgs []Package err = s.db.Preload("CPEs").Find(&pkgs).Error require.NoError(t, err) cpe1.ID = 1 cpe2.ID = 2 expPkg1 := *pkg1.Package expPkg1.ID = 1 expPkg1.CPEs = []Cpe{cpe1} expPkg2 := *pkg2.Package expPkg2.ID = 2 expPkg2.CPEs = []Cpe{cpe1, cpe2} expected := []Package{ expPkg1, expPkg2, } if d := cmp.Diff(expected, pkgs); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } expectedCPEs := []Cpe{cpe1, cpe2} var cpeResults []Cpe err = s.db.Find(&cpeResults).Error require.NoError(t, err) if d := cmp.Diff(expectedCPEs, cpeResults); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } }) } func TestUnaffectedPackageStore_GetUnaffectedPackages_ByCPE(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newUnaffectedPackageStore(db, bs, oss) cpe1 := Cpe{Part: "a", Vendor: "vendor1", Product: "product1"} cpe2 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2"} cpe3 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2", TargetSoftware: "target1"} pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-5678", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg2", Ecosystem: "type2", CPEs: []Cpe{cpe2}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } pkg3 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-5678", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg3", Ecosystem: "type2", CPEs: []Cpe{cpe3}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } err := s.AddUnaffectedPackages(pkg1, pkg2, pkg3) require.NoError(t, err) tests := []struct { name string cpe cpe.Attributes options *GetPackageOptions expected []UnaffectedPackageHandle wantErr require.ErrorAssertionFunc }{ { name: "full match CPE", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor1", Product: "product1", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: []UnaffectedPackageHandle{*pkg1}, }, { name: "partial match CPE", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor2", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: []UnaffectedPackageHandle{*pkg2, *pkg3}, }, { name: "match on any TSW when specific one provided when broad matching enabled", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor2", TargetSW: "target1", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, AllowBroadCPEMatching: true, }, expected: []UnaffectedPackageHandle{*pkg2, *pkg3}, }, { name: "do NOT match on any TSW when specific one provided when broad matching disabled", cpe: cpe.Attributes{ Part: "a", Vendor: "vendor2", TargetSW: "target1", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, AllowBroadCPEMatching: false, }, expected: []UnaffectedPackageHandle{*pkg3}, }, { name: "missing attributes", cpe: cpe.Attributes{ Part: "a", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: []UnaffectedPackageHandle{*pkg1, *pkg2, *pkg3}, }, { name: "no matches", cpe: cpe.Attributes{ Part: "a", Vendor: "unknown_vendor", Product: "unknown_product", }, options: &GetPackageOptions{ PreloadPackageCPEs: true, PreloadPackage: true, PreloadBlob: true, PreloadVulnerability: true, }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } result, err := s.GetUnaffectedPackages(&PackageSpecifier{CPE: &tt.cpe}, tt.options) tt.wantErr(t, err) if err != nil { return } if d := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); d != "" { t.Errorf("unexpected result: %s", d) } }) } } func TestUnaffectedPackageStore_GetUnaffectedPackages_CaseInsensitive(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newUnaffectedPackageStore(db, bs, oss) cpe1 := Cpe{Part: "a", Vendor: "Vendor1", Product: "Product1"} // capitalized pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, OperatingSystem: &OperatingSystem{ Name: "Ubuntu", // capitalized ReleaseID: "zubuntu", MajorVersion: "20", MinorVersion: "04", // leading 0 Codename: "focal", }, Package: &Package{Name: "Pkg1", Ecosystem: "Type1", CPEs: []Cpe{cpe1}}, // capitalized BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &UnaffectedPackageHandle{ // this should never register as a match Vulnerability: &VulnerabilityHandle{ Name: "CVE-2222-2222", Provider: &Provider{ ID: "provider2", }, }, OperatingSystem: &OperatingSystem{ Name: "ubuntu", ReleaseID: "ubuntu", MajorVersion: "20", MinorVersion: "10", }, Package: &Package{Name: "pkg2", Ecosystem: "type2"}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2222-2222"}, }, } err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) tests := []struct { name string pkgSpec *PackageSpecifier options *GetPackageOptions expected int }{ { name: "sanity check: search miss", pkgSpec: pkgFromName("does not exist"), expected: 0, }, { name: "get by name", pkgSpec: pkgFromName("pKG1"), expected: 1, }, { name: "get by CPE", pkgSpec: &PackageSpecifier{ CPE: &cpe.Attributes{Part: "a", Vendor: "veNDor1", Product: "pRODuct1"}, }, expected: 1, }, { name: "get by ecosystem", pkgSpec: &PackageSpecifier{ Ecosystem: "tYPE1", }, expected: 1, }, { name: "get by OS name and version (leading 0)", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "uBUNtu", MajorVersion: "20", MinorVersion: "04", }}, }, expected: 1, }, { name: "get by OS name and version", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "uBUNtu", MajorVersion: "20", MinorVersion: "4", }}, }, expected: 1, }, { name: "get by OS release", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "zUBuntu", }}, }, expected: 1, }, { name: "get by OS codename", options: &GetPackageOptions{ OSs: []*OSSpecifier{{ LabelVersion: "fOCAL", }}, }, expected: 1, }, { name: "get by vuln ID", options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{Name: "cVe-2023-1234"}}, }, expected: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := s.GetUnaffectedPackages(tt.pkgSpec, tt.options) require.NoError(t, err) require.Len(t, result, tt.expected) if tt.expected > 0 { assert.Equal(t, pkg1.PackageID, result[0].PackageID) } }) } } func TestUnaffectedPackageStore_GetUnaffectedPackages_MultipleVulnerabilitySpecs(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newUnaffectedPackageStore(db, bs, oss) cpe1 := Cpe{Part: "a", Vendor: "vendor1", Product: "product1"} cpe2 := Cpe{Part: "a", Vendor: "vendor2", Product: "product2"} pkg1 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg1", Ecosystem: "type1", CPEs: []Cpe{cpe1}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } pkg2 := &UnaffectedPackageHandle{ Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-5678", Provider: &Provider{ ID: "provider1", }, }, Package: &Package{Name: "pkg2", Ecosystem: "type2", CPEs: []Cpe{cpe2}}, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-5678"}, }, } err := s.AddUnaffectedPackages(pkg1, pkg2) require.NoError(t, err) result, err := s.GetUnaffectedPackages(nil, &GetPackageOptions{ PreloadVulnerability: true, Vulnerabilities: []VulnerabilitySpecifier{ {Name: "CVE-2023-1234"}, {Name: "CVE-2023-5678"}, }, }) require.NoError(t, err) actualVulns := strset.New() for _, r := range result { actualVulns.Add(r.Vulnerability.Name) } expectedVulns := strset.New("CVE-2023-1234", "CVE-2023-5678") assert.ElementsMatch(t, expectedVulns.List(), actualVulns.List()) } func TestUnaffectedPackageStore_GetUnaffectedPackages(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newUnaffectedPackageStore(db, bs, oss) pkg2d1 := testDistro1UnaffectedPackage2Handle() pkg2 := testNonDistroUnaffectedPackage2Handle() pkg2d2 := testDistro2UnaffectedPackage2Handle() err := s.AddUnaffectedPackages(pkg2d1, pkg2, pkg2d2) require.NoError(t, err) tests := []struct { name string pkg *PackageSpecifier options *GetPackageOptions expected []UnaffectedPackageHandle wantErr require.ErrorAssertionFunc }{ { name: "specific distro", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", }}, }, expected: []UnaffectedPackageHandle{*pkg2d1}, }, { name: "distro major version only", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", }}, }, expected: []UnaffectedPackageHandle{*pkg2d1, *pkg2d2}, }, { name: "distro codename", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{{ Name: "ubuntu", LabelVersion: "groovy", }}, }, expected: []UnaffectedPackageHandle{*pkg2d2}, }, { name: "no distro", pkg: pkgFromName(pkg2.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{NoOSSpecified}, }, expected: []UnaffectedPackageHandle{*pkg2}, }, { name: "any distro", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ OSs: []*OSSpecifier{AnyOSSpecified}, }, expected: []UnaffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2}, }, { name: "package type", pkg: &PackageSpecifier{Name: pkg2.Package.Name, Ecosystem: "type2"}, expected: []UnaffectedPackageHandle{*pkg2}, }, { name: "specific CVE", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ Name: "CVE-2023-1234", }}, }, expected: []UnaffectedPackageHandle{*pkg2d1}, }, { name: "any CVE published after a date", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ PublishedAfter: func() *time.Time { now := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC) return &now }(), }}, }, expected: []UnaffectedPackageHandle{*pkg2d1, *pkg2d2}, }, { name: "any CVE modified after a date", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ ModifiedAfter: func() *time.Time { now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC).Add(time.Hour * 2) return &now }(), }}, }, expected: []UnaffectedPackageHandle{*pkg2d1}, }, { name: "any rejected CVE", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetPackageOptions{ Vulnerabilities: []VulnerabilitySpecifier{{ Status: VulnerabilityRejected, }}, }, expected: []UnaffectedPackageHandle{*pkg2d1}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } for _, pc := range defaultUnaffectedPackageHandlePreloadCases() { t.Run(pc.name, func(t *testing.T) { opts := tt.options if opts == nil { opts = &GetPackageOptions{} } opts.PreloadOS = pc.PreloadOS opts.PreloadPackage = pc.PreloadPackage opts.PreloadBlob = pc.PreloadBlob opts.PreloadVulnerability = pc.PreloadVulnerability expected := tt.expected if pc.prepExpectations != nil { expected = pc.prepExpectations(t, expected) } result, err := s.GetUnaffectedPackages(tt.pkg, opts) tt.wantErr(t, err) if err != nil { return } if d := cmp.Diff(expected, result); d != "" { t.Errorf("unexpected result: %s", d) } }) } }) } } func TestUnaffectedPackageStore_ApplyPackageAlias(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) oss := newOperatingSystemStore(db, bs) s := newUnaffectedPackageStore(db, bs, oss) tests := []struct { name string input *PackageSpecifier expected string }{ // positive cases {name: "alias cocoapods", input: &PackageSpecifier{Ecosystem: "cocoapods"}, expected: "pod"}, {name: "alias pub", input: &PackageSpecifier{Ecosystem: "pub"}, expected: "dart-pub"}, {name: "alias otp", input: &PackageSpecifier{Ecosystem: "otp"}, expected: "erlang-otp"}, {name: "alias github", input: &PackageSpecifier{Ecosystem: "github"}, expected: "github-action"}, {name: "alias golang", input: &PackageSpecifier{Ecosystem: "golang"}, expected: "go-module"}, {name: "alias maven", input: &PackageSpecifier{Ecosystem: "maven"}, expected: "java-archive"}, {name: "alias composer", input: &PackageSpecifier{Ecosystem: "composer"}, expected: "php-composer"}, {name: "alias pecl", input: &PackageSpecifier{Ecosystem: "pecl"}, expected: "php-pecl"}, {name: "alias pypi", input: &PackageSpecifier{Ecosystem: "pypi"}, expected: "python"}, {name: "alias cran", input: &PackageSpecifier{Ecosystem: "cran"}, expected: "R-package"}, {name: "alias luarocks", input: &PackageSpecifier{Ecosystem: "luarocks"}, expected: "lua-rocks"}, {name: "alias cargo", input: &PackageSpecifier{Ecosystem: "cargo"}, expected: "rust-crate"}, // negative cases {name: "generic type", input: &PackageSpecifier{Ecosystem: "generic/linux-kernel"}, expected: "generic/linux-kernel"}, {name: "empty ecosystem", input: &PackageSpecifier{Ecosystem: ""}, expected: ""}, {name: "matching type", input: &PackageSpecifier{Ecosystem: "python"}, expected: "python"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := s.pkgStore.applyPackageAlias(tt.input) require.NoError(t, err) assert.Equal(t, tt.expected, tt.input.Ecosystem) }) } } func testDistro1UnaffectedPackage2Handle() *UnaffectedPackageHandle { now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC) later := now.Add(time.Hour * 200) return &UnaffectedPackageHandle{ Package: &Package{ Name: "pkg2", Ecosystem: "type2d", }, Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-1234", Status: VulnerabilityRejected, PublishedDate: &now, ModifiedDate: &later, Provider: &Provider{ ID: "ubuntu", }, }, OperatingSystem: &OperatingSystem{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", LabelVersion: "focal", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-1234"}, }, } } func testDistro2UnaffectedPackage2Handle() *UnaffectedPackageHandle { now := time.Date(2020, 1, 1, 3, 4, 5, 0, time.UTC) later := now.Add(time.Hour * 200) return &UnaffectedPackageHandle{ Package: &Package{ Name: "pkg2", Ecosystem: "type2d", }, Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-4567", PublishedDate: &now, ModifiedDate: &later, Provider: &Provider{ ID: "ubuntu", }, }, OperatingSystem: &OperatingSystem{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "10", LabelVersion: "groovy", }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-4567"}, }, } } func testNonDistroUnaffectedPackage2Handle() *UnaffectedPackageHandle { now := time.Date(2005, 1, 1, 3, 4, 5, 0, time.UTC) later := now.Add(time.Hour * 200) return &UnaffectedPackageHandle{ Package: &Package{ Name: "pkg2", Ecosystem: "type2", }, Vulnerability: &VulnerabilityHandle{ Name: "CVE-2023-4567", PublishedDate: &now, ModifiedDate: &later, Provider: &Provider{ ID: "wolfi", }, }, BlobValue: &PackageBlob{ CVEs: []string{"CVE-2023-4567"}, }, } } ================================================ FILE: grype/db/v6/vulnerability.go ================================================ package v6 import ( "fmt" "sort" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/pkg/qualifier/platformcpe" "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" ) const ( nvdProvider = "nvd" githubProvider = "github" v5NvdNamespace = "nvd:cpe" ) func newVulnerabilityFromAffectedPackageHandle(affected AffectedPackageHandle, affectedRanges []Range) (*vulnerability.Vulnerability, error) { packageName := "" if affected.Package != nil { packageName = affected.Package.Name } if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil { return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedPackageHandle") } return newVulnerabilityFromParts(packageName, affected.Vulnerability, affected.BlobValue, affectedRanges, &affected, nil) } func newVulnerabilityFromAffectedCPEHandle(affected AffectedCPEHandle, affectedRanges []Range) (*vulnerability.Vulnerability, error) { if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil { return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedCPEHandle") } return newVulnerabilityFromParts(affected.CPE.Product, affected.Vulnerability, affected.BlobValue, affectedRanges, nil, &affected) } func newVulnerabilityFromUnaffectedPackageHandle(unaffected UnaffectedPackageHandle, unaffectedRanges []Range) (*vulnerability.Vulnerability, error) { packageName := "" if unaffected.Package != nil { packageName = unaffected.Package.Name } if unaffected.Vulnerability == nil || unaffected.Vulnerability.BlobValue == nil || unaffected.BlobValue == nil { return nil, fmt.Errorf("nil data when attempting to create vulnerability from UnaffectedPackageHandle") } vuln, err := newVulnerabilityFromParts(packageName, unaffected.Vulnerability, unaffected.BlobValue, unaffectedRanges, (*AffectedPackageHandle)(&unaffected), nil) if vuln != nil { vuln.Unaffected = true } return vuln, err } func newVulnerabilityFromUnaffectedCPEHandle(unaffected UnaffectedCPEHandle, unaffectedRanges []Range) (*vulnerability.Vulnerability, error) { if unaffected.Vulnerability == nil || unaffected.Vulnerability.BlobValue == nil || unaffected.BlobValue == nil { return nil, fmt.Errorf("nil data when attempting to create vulnerability from UnaffectedCPEHandle") } vuln, err := newVulnerabilityFromParts(unaffected.CPE.Product, unaffected.Vulnerability, unaffected.BlobValue, unaffectedRanges, nil, (*AffectedCPEHandle)(&unaffected)) if vuln != nil { vuln.Unaffected = true } return vuln, err } func newVulnerabilityFromParts(packageName string, vuln *VulnerabilityHandle, pkgBlob *PackageBlob, ranges []Range, affectedPackageHandle *AffectedPackageHandle, affectedCpeHandle *AffectedCPEHandle) (*vulnerability.Vulnerability, error) { if vuln.BlobValue == nil { return nil, fmt.Errorf("vuln has no blob value: %+v", vuln) } constraint, err := getVersionConstraint(ranges) if err != nil { return nil, nil } var language string if affectedPackageHandle != nil && affectedPackageHandle.Package != nil { language = affectedPackageHandle.Package.Ecosystem } v5namespace := MimicV5Namespace(vuln, affectedPackageHandle) return &vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: vuln.Name, Namespace: v5namespace, Internal: vuln, // just hold a reference to the vulnHandle for later use }, PackageName: packageName, PackageQualifiers: getPackageQualifiers(pkgBlob), Constraint: constraint, CPEs: toCPEs(affectedPackageHandle, affectedCpeHandle), RelatedVulnerabilities: getRelatedVulnerabilities(vuln, pkgBlob, language), Fix: toFix(ranges), Advisories: toAdvisories(ranges), Status: string(vuln.Status), }, nil } func getVersionConstraint(affectedRanges []Range) (version.Constraint, error) { var constraints []string types := strset.New() for _, r := range affectedRanges { if r.Version.Constraint != "" { if r.Version.Type != "" { types.Add(r.Version.Type) } constraints = append(constraints, r.Version.Constraint) } } if types.Size() > 1 { log.WithFields("types", types.List()).Debug("multiple version formats found for a single vulnerability") } var ty string if types.Size() >= 1 { typeStrs := types.List() sort.Strings(typeStrs) ty = typeStrs[0] } versionFormat := version.ParseFormat(ty) constraint, err := version.GetConstraint(strings.Join(constraints, ","), versionFormat) if err != nil { log.WithFields("error", err, "constraint", constraints).Debug("unable to parse constraint") return nil, err } return constraint, nil } // getRelatedVulnerabilities returns a list of related vulnerabilities based on the vulnerability ID, aliases, and CVEs from the affected package (if available). func getRelatedVulnerabilities(vuln *VulnerabilityHandle, affected *PackageBlob, language string) []vulnerability.Reference { var relatedVulnerabilities []vulnerability.Reference idsToProcess := append([]string{vuln.Name}, vuln.BlobValue.Aliases...) if affected != nil { idsToProcess = append(idsToProcess, affected.CVEs...) } encountered := strset.New() for _, id := range idsToProcess { if encountered.Has(id) { continue } lowerID := strings.ToLower(id) switch { case strings.HasPrefix(lowerID, "cve-"): if vuln.ProviderID == nvdProvider && strings.EqualFold(vuln.Name, id) { // the original vuln is an NVD CVE, so we don't need to add a self-reference continue } relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{ ID: id, Namespace: v5NvdNamespace, }, ) case strings.HasPrefix(lowerID, "ghsa-"): if vuln.ProviderID == githubProvider && strings.EqualFold(vuln.Name, id) { // the original vuln is a GitHub GHSA, so we don't need to add a self-reference continue } relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{ ID: id, Namespace: mimicV5GithubNamespace(githubProvider, language), }, ) } encountered.Add(id) } return relatedVulnerabilities } func getPackageQualifiers(affected *PackageBlob) []qualifier.Qualifier { if affected != nil { return toPackageQualifiers(affected.Qualifiers) } return nil } // MimicV5Namespace returns the namespace for a given affected package based on what schema v5 did. // //nolint:funlen func MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle) string { if affected == nil || affected.Package == nil { // for CPE matches return fmt.Sprintf("%s:cpe", vuln.Provider.ID) } if affected.OperatingSystem != nil { // distro family fixes family := affected.OperatingSystem.Name ver := affected.OperatingSystem.Version() switch affected.OperatingSystem.Name { case "amazon": family = "amazonlinux" case "mariner", "azurelinux": fields := strings.Split(ver, ".") major := fields[0] switch len(fields) { case 1: ver = fmt.Sprintf("%s.0", major) default: ver = fmt.Sprintf("%s.%s", major, fields[1]) } switch major { case "1", "2": family = "mariner" default: family = "azurelinux" } case "ubuntu": if strings.Count(ver, ".") == 1 { // convert 20.4 to 20.04 fields := strings.Split(ver, ".") major, minor := fields[0], fields[1] if len(minor) == 1 { ver = fmt.Sprintf("%s.0%s", major, minor) } } case "oracle": family = "oraclelinux" } // provider fixes pr := vuln.Provider.ID if pr == "rhel" { pr = "redhat" } // version fixes switch vuln.Provider.ID { case "rhel", "oracle": // ensure we only keep the major version ver = strings.Split(ver, ".")[0] } return fmt.Sprintf("%s:distro:%s:%s", pr, family, ver) } if affected.Package != nil { language := affected.Package.Ecosystem switch strings.ToLower(language) { case "msrc", string(pkg.KbPkg): // msrc packages were previously modelled as distro return fmt.Sprintf("%s:distro:windows:%s", vuln.Provider.ID, affected.Package.Name) case string(pkg.BitnamiPkg): // bitnami packages were previously modelled as distro return "bitnami" case "": // CPE return fmt.Sprintf("%s:cpe", vuln.Provider.ID) } return mimicV5GithubNamespace(vuln.Provider.ID, affected.Package.Ecosystem) } // this shouldn't happen and is not a valid v5 namespace, but some information is better than none return vuln.Provider.ID } func mimicV5GithubNamespace(provider, language string) string { // normalize from purl type, github ecosystem types, and vunnel mappings switch strings.ToLower(language) { case "golang", string(pkg.GoModulePkg): language = "go" case "composer", string(pkg.PhpComposerPkg): language = "php" case "cargo", string(pkg.RustPkg): language = "rust" case "pub", string(pkg.DartPubPkg): language = "dart" case "nuget", string(pkg.DotnetPkg): language = "dotnet" case "maven", string(pkg.JavaPkg), string(pkg.JenkinsPluginPkg): language = "java" case "swifturl", string(pkg.SwiplPackPkg), string(pkg.SwiftPkg): language = "swift" case "node", string(pkg.NpmPkg): language = "javascript" case "pypi", "pip", string(pkg.PythonPkg): language = "python" case "rubygems", string(pkg.GemPkg): language = "ruby" } return fmt.Sprintf("%s:language:%s", provider, language) } func toPackageQualifiers(qualifiers *PackageQualifiers) []qualifier.Qualifier { if qualifiers == nil { return nil } var out []qualifier.Qualifier for _, c := range qualifiers.PlatformCPEs { out = append(out, platformcpe.New(c)) } if qualifiers.RpmModularity != nil { out = append(out, rpmmodularity.New(*qualifiers.RpmModularity)) } return out } func toFix(affectedRanges []Range) vulnerability.Fix { var state vulnerability.FixState var versions []string var availables []vulnerability.FixAvailable for _, r := range affectedRanges { if r.Fix == nil { continue } switch r.Fix.State { case FixedStatus: state = vulnerability.FixStateFixed versions = append(versions, r.Fix.Version) if r.Fix.Detail != nil && r.Fix.Detail.Available != nil { a := r.Fix.Detail.Available if a.Date != nil { availables = append(availables, vulnerability.FixAvailable{ Version: r.Fix.Version, Date: *a.Date, Kind: a.Kind, }) } } case NotAffectedFixStatus: // TODO: not handled yet case WontFixStatus: if state != vulnerability.FixStateFixed { state = vulnerability.FixStateWontFix } case NotFixedStatus: if state != vulnerability.FixStateFixed { state = vulnerability.FixStateNotFixed } } } if len(versions) == 0 && state == "" { return vulnerability.Fix{} } return vulnerability.Fix{ Versions: versions, State: state, Available: availables, } } func toAdvisories(affectedRanges []Range) []vulnerability.Advisory { var advisories []vulnerability.Advisory for _, r := range affectedRanges { if r.Fix == nil || r.Fix.Detail == nil { continue } for _, urlRef := range r.Fix.Detail.References { if urlRef.URL == "" { continue } advisories = append(advisories, vulnerability.Advisory{ ID: urlRef.ID, Link: urlRef.URL, }) } } return advisories } func toCPEs(affectedPackageHandle *AffectedPackageHandle, affectedCPEHandle *AffectedCPEHandle) []cpe.CPE { var out []cpe.CPE var cpes []Cpe if affectedPackageHandle != nil { cpes = affectedPackageHandle.Package.CPEs } if affectedCPEHandle != nil && affectedCPEHandle.CPE != nil { cpes = append(cpes, *affectedCPEHandle.CPE) } for _, c := range cpes { out = append(out, cpe.CPE{ Attributes: cpe.Attributes{ Part: c.Part, Vendor: c.Vendor, Product: c.Product, Version: cpe.Any, Update: cpe.Any, Edition: c.Edition, SWEdition: c.SoftwareEdition, TargetSW: c.TargetSoftware, TargetHW: c.TargetHardware, Other: c.Other, Language: c.Language, }, Source: "", }) } return out } ================================================ FILE: grype/db/v6/vulnerability_decorator_store.go ================================================ package v6 import ( "fmt" "time" "gorm.io/gorm" "github.com/anchore/go-logger" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/schemaver" ) type VulnerabilityDecoratorStoreWriter interface { AddKnownExploitedVulnerabilities(...*KnownExploitedVulnerabilityHandle) error AddEpss(...*EpssHandle) error AddCWE(...*CWEHandle) error } type VulnerabilityDecoratorStoreReader interface { GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error) GetEpss(cve string) ([]EpssHandle, error) GetCWEs(cve string) ([]CWEHandle, error) } type vulnerabilityDecoratorStore struct { db *gorm.DB blobStore *blobStore epssDate *time.Time vulnerabilityDecoratorCapabilities } type vulnerabilityDecoratorCapabilities struct { kevEnabled bool epssEnabled bool cweEnabled bool } func newVulnerabilityDecoratorStore(db *gorm.DB, bs *blobStore, dbVersion schemaver.SchemaVer) *vulnerabilityDecoratorStore { minSupportedKEVClientVersion := schemaver.New(6, 0, 1) minSupportedEPSSClientVersion := schemaver.New(6, 0, 2) minSupportedCWEClientVersion := schemaver.New(6, 1, 2) return &vulnerabilityDecoratorStore{ db: db, blobStore: bs, vulnerabilityDecoratorCapabilities: vulnerabilityDecoratorCapabilities{ kevEnabled: dbVersion.GreaterOrEqualTo(minSupportedKEVClientVersion), epssEnabled: dbVersion.GreaterOrEqualTo(minSupportedEPSSClientVersion), cweEnabled: dbVersion.GreaterOrEqualTo(minSupportedCWEClientVersion), }, } } func (s *vulnerabilityDecoratorStore) AddEpss(epss ...*EpssHandle) error { if !s.epssEnabled { // when populating a new DB any capability issues found should result in halting return ErrDBCapabilityNotSupported } for i := range epss { e := epss[i] if err := s.db.Create(e).Error; err != nil { return fmt.Errorf("unable to create EPSS: %w", err) } if err := s.setEPSSMetadata(e.Date); err != nil { return fmt.Errorf("unable to set EPSS metadata: %w", err) } } return nil } func (s *vulnerabilityDecoratorStore) AddCWE(cwe ...*CWEHandle) error { if !s.cweEnabled { // when populating a new DB any capability issues found should result in halting return ErrDBCapabilityNotSupported } for i := range cwe { c := cwe[i] if err := s.db.Create(c).Error; err != nil { return fmt.Errorf("unable to create CWE: %w", err) } } return nil } func (s *vulnerabilityDecoratorStore) setEPSSMetadata(date time.Time) error { if !s.epssEnabled { // when populating a new DB any capability issues found should result in halting return ErrDBCapabilityNotSupported } if s.epssDate != nil { if s.epssDate.Equal(date) { return nil } return fmt.Errorf("observed multiple EPSS dates: current=%q new=%q", s.epssDate.String(), date.String()) } log.Trace("writing EPSS metadata") if err := s.db.Where("true").Delete(&EpssMetadata{}).Error; err != nil { return fmt.Errorf("failed to delete existing EPSS metadata record: %w", err) } instance := &EpssMetadata{ Date: date, } if err := s.db.Create(instance).Error; err != nil { return fmt.Errorf("failed to create EPSS metadata record: %w", err) } s.epssDate = &date return nil } func (s *vulnerabilityDecoratorStore) getEPSSMetadata() (*EpssMetadata, error) { log.Trace("fetching EPSS metadata") var model EpssMetadata result := s.db.First(&model) return &model, result.Error } func (s *vulnerabilityDecoratorStore) GetEpss(cve string) ([]EpssHandle, error) { if !s.epssEnabled { // capability incompatibilities should gracefully degrade, returning no data or errors return nil, nil } fields := logger.Fields{ "cve": cve, } start := time.Now() var count int defer func() { fields["duration"] = time.Since(start) fields["records"] = count log.WithFields(fields).Trace("fetched EPSS records") }() var models []EpssHandle var results []*EpssHandle if s.epssDate == nil { // fetch and cache the EPSS metadata metadata, err := s.getEPSSMetadata() if err != nil { return nil, fmt.Errorf("unable to fetch EPSS metadata: %w", err) } s.epssDate = &metadata.Date } if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { for _, r := range results { r.Date = *s.epssDate models = append(models, *r) } count += len(results) return nil }).Error; err != nil { return models, fmt.Errorf("unable to fetch EPSS records: %w", err) } return models, nil } func (s *vulnerabilityDecoratorStore) GetCWEs(cve string) ([]CWEHandle, error) { if !s.cweEnabled { // capability incompatibilities should gracefully degrade, returning no data or errors return nil, nil } fields := logger.Fields{ "cve": cve, } start := time.Now() var count int defer func() { fields["duration"] = time.Since(start) fields["records"] = count log.WithFields(fields).Trace("fetched CWE records") }() var models []CWEHandle var results []*CWEHandle if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { for _, r := range results { models = append(models, *r) } count += len(results) return nil }).Error; err != nil { return models, fmt.Errorf("unable to fetch CWE records: %w", err) } return models, nil } func (s *vulnerabilityDecoratorStore) AddKnownExploitedVulnerabilities(kevs ...*KnownExploitedVulnerabilityHandle) error { if !s.kevEnabled { // when populating a new DB any capability issues found should result in halting return ErrDBCapabilityNotSupported } for i := range kevs { k := kevs[i] // this adds the blob value to the DB and sets the ID on the kev handle if err := s.blobStore.addBlobable(k); err != nil { return fmt.Errorf("unable to add KEV blob: %w", err) } if err := s.db.Create(k).Error; err != nil { return fmt.Errorf("unable to create known exploited vulnerability: %w", err) } } return nil } func (s *vulnerabilityDecoratorStore) GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error) { if !s.kevEnabled { // capability incompatibilities should gracefully degrade, returning no data or errors return nil, nil } fields := logger.Fields{ "cve": cve, } start := time.Now() var count int defer func() { fields["duration"] = time.Since(start) fields["records"] = count log.WithFields(fields).Trace("fetched KEV records") }() var models []KnownExploitedVulnerabilityHandle var results []*KnownExploitedVulnerabilityHandle if err := s.db.Where("cve = ? collate nocase", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { var blobs []blobable for _, r := range results { blobs = append(blobs, r) } if err := s.blobStore.attachBlobValue(blobs...); err != nil { return fmt.Errorf("unable to attach KEV blobs: %w", err) } for _, r := range results { models = append(models, *r) } count += len(results) return nil }).Error; err != nil { return models, fmt.Errorf("unable to fetch KEV records: %w", err) } return models, nil } ================================================ FILE: grype/db/v6/vulnerability_decorator_store_test.go ================================================ package v6 import ( "reflect" "slices" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/internal/schemaver" ) func TestVulnerabilityDecoratorStore(t *testing.T) { tests := []struct { name string kevEnabled bool setupStore func(*vulnerabilityDecoratorStore) error input []*KnownExploitedVulnerabilityHandle expectError require.ErrorAssertionFunc }{ { name: "happy path - single KEV", kevEnabled: true, input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", VendorProject: "Test Vendor", Product: "Test Product", DateAdded: timeRef(time.Now()), }, }, }, }, { name: "happy path - multiple KEVs", kevEnabled: true, input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", VendorProject: "Vendor 1", }, }, { Cve: "CVE-2023-5678", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-5678", VendorProject: "Vendor 2", }, }, }, }, { name: "error - KEV disabled", kevEnabled: false, input: []*KnownExploitedVulnerabilityHandle{{Cve: "CVE-2023-1234"}}, expectError: require.Error, }, { name: "duplicate CVEs (unexpected but allowed)", kevEnabled: true, input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", RequiredAction: "1", }, }, { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", RequiredAction: "2", }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectError == nil { tt.expectError = require.NoError } db := setupTestStore(t).db bs := newBlobStore(db) s := &vulnerabilityDecoratorStore{ db: db, blobStore: bs, vulnerabilityDecoratorCapabilities: vulnerabilityDecoratorCapabilities{ kevEnabled: tt.kevEnabled, }, } if tt.setupStore != nil { require.NoError(t, tt.setupStore(s)) } err := s.AddKnownExploitedVulnerabilities(tt.input...) tt.expectError(t, err) if err != nil { return } var cves []string for _, kev := range tt.input { if !slices.Contains(cves, kev.Cve) { cves = append(cves, kev.Cve) } } var actual []*KnownExploitedVulnerabilityHandle for _, cve := range cves { intermediate, err := s.GetKnownExploitedVulnerabilities(cve) require.NoError(t, err) for i := range intermediate { actual = append(actual, &intermediate[i]) } } for _, a := range actual { assert.NotZero(t, a.ID) assert.NotZero(t, a.BlobID) } if d := cmp.Diff(tt.input, actual); d != "" { t.Errorf("unexpected known exploited vulnerabilities (-expected, +actual): %s", d) } }) } } func TestVulnerabilityDecoratorStore_AddKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) { tests := []struct { name string dbVersion schemaver.SchemaVer input []*KnownExploitedVulnerabilityHandle expectEnabled bool expectError require.ErrorAssertionFunc expectedCount int }{ { name: "supported db version", dbVersion: schemaver.New(6, 0, 1), input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", VendorProject: "Test Vendor", DateAdded: timeRef(time.Now()), }, }, }, expectEnabled: true, expectError: require.NoError, expectedCount: 1, }, { name: "unsupported db version", dbVersion: schemaver.New(6, 0, 0), input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", }, }, }, expectEnabled: false, expectError: require.Error, expectedCount: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectError == nil { tt.expectError = require.NoError } db := setupTestStore(t).db bs := newBlobStore(db) s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion) assert.Equal(t, tt.expectEnabled, s.kevEnabled) err := s.AddKnownExploitedVulnerabilities(tt.input...) tt.expectError(t, err) if err != nil { return } results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve) require.NoError(t, err) assert.Len(t, results, tt.expectedCount) }) } } func TestVulnerabilityDecoratorStore_GetKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) { tests := []struct { name string dbVersion schemaver.SchemaVer input []*KnownExploitedVulnerabilityHandle expectEnabled bool expectError require.ErrorAssertionFunc expectedCount int }{ { name: "supported db version", dbVersion: schemaver.New(6, 0, 1), input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", }, }, }, expectEnabled: true, expectError: require.NoError, expectedCount: 1, }, { name: "unsupported db version", dbVersion: schemaver.New(6, 0, 0), input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", }, }, }, expectEnabled: false, expectError: require.NoError, expectedCount: 0, }, { name: "future db version", dbVersion: schemaver.New(6, 1, 0), input: []*KnownExploitedVulnerabilityHandle{ { Cve: "CVE-2023-1234", BlobValue: &KnownExploitedVulnerabilityBlob{ Cve: "CVE-2023-1234", }, }, }, expectEnabled: true, expectError: require.NoError, expectedCount: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectError == nil { tt.expectError = require.NoError } db := setupTestStore(t).db bs := newBlobStore(db) s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion) assert.Equal(t, tt.expectEnabled, s.kevEnabled) // this is just to get around not being able to write entries... supportedStore := newVulnerabilityDecoratorStore(db, bs, schemaver.New(6, 0, 1)) err := supportedStore.AddKnownExploitedVulnerabilities(tt.input...) require.NoError(t, err) results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve) tt.expectError(t, err) assert.Len(t, results, tt.expectedCount) if tt.expectedCount > 0 { for _, result := range results { assert.NotNil(t, result.BlobValue) assert.Equal(t, tt.input[0].Cve, result.BlobValue.Cve) } } }) } } func TestVulnerabilityDecoratorCapabilities_AllCapabilitiesCovered(t *testing.T) { tests := []struct { capability string minSupportedVersion schemaver.SchemaVer fieldName string }{ { capability: "KEV", minSupportedVersion: schemaver.New(6, 0, 1), fieldName: "kevEnabled", }, { capability: "EPSS", minSupportedVersion: schemaver.New(6, 0, 2), fieldName: "epssEnabled", }, { capability: "CWE", minSupportedVersion: schemaver.New(6, 1, 2), fieldName: "cweEnabled", }, } // Verify all fields in vulnerabilityDecoratorCapabilities are covered capabilitiesType := reflect.TypeOf(vulnerabilityDecoratorCapabilities{}) fieldCount := capabilitiesType.NumField() if fieldCount != len(tests) { t.Errorf("vulnerabilityDecoratorCapabilities has %d fields but only %d test cases. All fields must be covered.", fieldCount, len(tests)) } coveredFields := make(map[string]bool) for _, tt := range tests { coveredFields[tt.fieldName] = true } for i := 0; i < fieldCount; i++ { field := capabilitiesType.Field(i) if !coveredFields[field.Name] { t.Errorf("Field %q in vulnerabilityDecoratorCapabilities is not covered by any test case", field.Name) } } // Test each capability at lower, equal, and higher versions for _, tt := range tests { t.Run(tt.capability+"_version_checks", func(t *testing.T) { db := setupTestStore(t).db bs := newBlobStore(db) // Test 1: Lower version - capability should be OFF lowerVersion := schemaver.New( tt.minSupportedVersion.Model, tt.minSupportedVersion.Revision, tt.minSupportedVersion.Addition-1, ) t.Run("lower_version", func(t *testing.T) { store := newVulnerabilityDecoratorStore(db, bs, lowerVersion) capValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName) assert.False(t, capValue.Bool(), "capability %s should be disabled for version %s (below %s)", tt.capability, lowerVersion, tt.minSupportedVersion) }) // Test 2: Equal version - capability should be ON t.Run("equal_version", func(t *testing.T) { store := newVulnerabilityDecoratorStore(db, bs, tt.minSupportedVersion) capValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName) assert.True(t, capValue.Bool(), "capability %s should be enabled for version %s", tt.capability, tt.minSupportedVersion) }) // Test 3: Higher version - capability should be ON higherVersion := schemaver.New( tt.minSupportedVersion.Model, tt.minSupportedVersion.Revision+1, 0, ) t.Run("higher_version", func(t *testing.T) { store := newVulnerabilityDecoratorStore(db, bs, higherVersion) capValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName) assert.True(t, capValue.Bool(), "capability %s should be enabled for version %s (above %s)", tt.capability, higherVersion, tt.minSupportedVersion) }) }) } } func timeRef(t time.Time) *time.Time { return &t } ================================================ FILE: grype/db/v6/vulnerability_provider.go ================================================ package v6 import ( "errors" "fmt" "io" "strings" "time" "github.com/hashicorp/go-multierror" "github.com/iancoleman/strcase" "github.com/scylladb/go-set/strset" "github.com/anchore/go-logger" "github.com/anchore/grype/grype/db/v6/name" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) var ( _ vulnerability.Provider = (*vulnerabilityProvider)(nil) _ vulnerability.StoreMetadataProvider = (*vulnerabilityProvider)(nil) _ vulnerability.EOLChecker = (*vulnerabilityProvider)(nil) ) func NewVulnerabilityProvider(rdr Reader) vulnerability.Provider { return &vulnerabilityProvider{ reader: rdr, } } type vulnerabilityProvider struct { reader Reader } // Deprecated: vulnerability.Vulnerability objects now have metadata included func (vp vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { vuln, ok := ref.Internal.(*VulnerabilityHandle) if !ok { var err error vuln, err = vp.fetchVulnerability(ref) if err != nil { return nil, err } } if vuln == nil { log.WithFields("id", ref.ID, "namespace", ref.Namespace).Debug("unable to find vulnerability for given reference") return &vulnerability.Metadata{ ID: ref.ID, DataSource: strings.Split(ref.Namespace, ":")[0], Namespace: ref.Namespace, Severity: toSeverityString(vulnerability.UnknownSeverity), }, nil } return vp.getVulnerabilityMetadata(vuln, ref.Namespace) } func (vp vulnerabilityProvider) getVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) { cves := getCVEs(vuln) kevs, err := vp.fetchKnownExploited(cves) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch known exploited from vulnerability") } epss, err := vp.fetchEpss(cves) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch epss from vulnerability") } cwes, err := vp.fetchCWE(cves) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch cwes from vulnerability") } return newVulnerabilityMetadata(vuln, namespace, kevs, epss, cwes) } func (vp vulnerabilityProvider) fetchCWE(cves []string) ([]vulnerability.CWE, error) { var out []vulnerability.CWE var errs error for _, cve := range cves { entries, err := vp.reader.GetCWEs(cve) if err != nil { errs = multierror.Append(errs, err) continue } for _, entry := range entries { out = append(out, vulnerability.CWE{ CVE: entry.CVE, CWE: entry.CWE, Source: entry.Source, Type: entry.Type, }) } } return out, errs } func newVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string, kevs []vulnerability.KnownExploited, epss []vulnerability.EPSS, cwes []vulnerability.CWE) (*vulnerability.Metadata, error) { if vuln == nil { return nil, nil } sev, cvss, err := extractSeverities(vuln) if err != nil { log.WithFields("id", vuln.Name, "vulnerability", vuln.String()).Debug("unable to extract severity from vulnerability") } return &vulnerability.Metadata{ ID: vuln.Name, DataSource: firstReferenceURL(vuln), Namespace: namespace, Severity: toSeverityString(sev), URLs: lastReferenceURLs(vuln), Description: vuln.BlobValue.Description, Cvss: cvss, KnownExploited: kevs, EPSS: epss, CWEs: cwes, }, nil } func (vp vulnerabilityProvider) DataProvenance() (map[string]vulnerability.DataProvenance, error) { providers, err := vp.reader.AllProviders() if err != nil { return nil, err } dps := make(map[string]vulnerability.DataProvenance) for _, p := range providers { var date time.Time if p.DateCaptured != nil { date = *p.DateCaptured } dps[p.ID] = vulnerability.DataProvenance{ DateCaptured: date, InputDigest: p.InputDigest, } } return dps, nil } func (vp vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) (*VulnerabilityHandle, error) { provider := strings.Split(ref.Namespace, ":")[0] vulns, err := vp.reader.GetVulnerabilities(&VulnerabilitySpecifier{Name: ref.ID, Providers: []string{provider}}, &GetVulnerabilityOptions{Preload: true}) if err != nil { return nil, err } if len(vulns) > 0 { return &vulns[0], nil } return nil, nil } func (vp vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) { var out []vulnerability.KnownExploited var errs error for _, cve := range cves { kevs, err := vp.reader.GetKnownExploitedVulnerabilities(cve) if err != nil { errs = multierror.Append(errs, err) continue } for _, kev := range kevs { out = append(out, vulnerability.KnownExploited{ CVE: kev.Cve, VendorProject: kev.BlobValue.VendorProject, Product: kev.BlobValue.Product, DateAdded: kev.BlobValue.DateAdded, RequiredAction: kev.BlobValue.RequiredAction, DueDate: kev.BlobValue.DueDate, KnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse, Notes: kev.BlobValue.Notes, URLs: kev.BlobValue.URLs, CWEs: kev.BlobValue.CWEs, }) } } return out, errs } func (vp vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) { var out []vulnerability.EPSS var errs error for _, cve := range cves { entries, err := vp.reader.GetEpss(cve) if err != nil { errs = multierror.Append(errs, err) continue } for _, entry := range entries { out = append(out, vulnerability.EPSS{ CVE: entry.Cve, EPSS: entry.Epss, Percentile: entry.Percentile, Date: entry.Date, }) } } return out, errs } func (vp vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string { return name.PackageNames(p) } func (vp vulnerabilityProvider) Close() error { return vp.reader.(io.Closer).Close() } func (vp vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { if err := search.ValidateCriteria(criteria); err != nil { return nil, err } var out []vulnerability.Vulnerability for _, criteriaSet := range search.CriteriaIterator(criteria) { // parse criteria into a search query object query, remainingCriteria, err := newSearchQuery(criteriaSet) if err != nil { return nil, err } // fetch and process packages pkgVulns, err := vp.fetchAndProcessPackages(query) if err != nil { return nil, err } // fetch and process CPEs cpeVulns, err := vp.fetchAndProcessCPEs(query) if err != nil { return nil, err } // combine vulnerabilities var vulns []vulnerability.Vulnerability vulns = append(vulns, pkgVulns...) vulns = append(vulns, cpeVulns...) // apply remaining filters vulns, err = vp.filterVulnerabilities(vulns, remainingCriteria...) if err != nil { return nil, err } out = append(out, vulns...) } return out, nil } // fetchAndProcessPackages fetches packages and returns vulnerabilities directly func (vp vulnerabilityProvider) fetchAndProcessPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) { // only fetch if we have package specifications or vulnerability specifications if query.pkgSpec == nil && len(query.vulnSpecs) == 0 { return nil, nil } if query.unaffectedOnly { return vp.fetchAndProcessUnaffectedPackages(query) } return vp.fetchAndProcessAffectedPackages(query) } // fetchAndProcessAffectedPackages fetches affected packages and returns vulnerabilities func (vp vulnerabilityProvider) fetchAndProcessAffectedPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) { var vulns []vulnerability.Vulnerability metadataCache := make(map[string]*vulnerability.Metadata) affectedPackages, err := vp.reader.GetAffectedPackages(query.pkgSpec, &GetPackageOptions{ OSs: query.osSpecs, Vulnerabilities: query.vulnSpecs, PreloadBlob: true, }) if err != nil { if err = vp.handleOSError(err, query.osSpecs); err != nil { return nil, err } // if handleOSError returned nil, it means ErrOSNotPresent was handled return vulns, nil } affectedPackages = filterAffectedPackageVersions(query.versionMatcher, affectedPackages) if err = fillAffectedPackageHandles(vp.reader, ptrs(affectedPackages)); err != nil { return nil, err } vulns, err = vp.processAffectedPackageHandles(vulns, affectedPackages, metadataCache) if err != nil { return nil, err } return vulns, nil } // fetchAndProcessUnaffectedPackages fetches unaffected packages and returns vulnerabilities func (vp vulnerabilityProvider) fetchAndProcessUnaffectedPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) { var vulns []vulnerability.Vulnerability metadataCache := make(map[string]*vulnerability.Metadata) unaffectedPackages, err := vp.reader.GetUnaffectedPackages(query.pkgSpec, &GetPackageOptions{ OSs: query.osSpecs, Vulnerabilities: query.vulnSpecs, PreloadBlob: true, }) if err != nil { if err = vp.handleOSError(err, query.osSpecs); err != nil { return nil, err } // if handleOSError returned nil, it means ErrOSNotPresent was handled return vulns, nil } unaffectedPackages = filterUnaffectedPackageVersions(query.versionMatcher, unaffectedPackages) if err = fillUnaffectedPackageHandles(vp.reader, ptrs(unaffectedPackages)); err != nil { return nil, err } vulns, err = vp.processUnaffectedPackageHandles(vulns, unaffectedPackages, metadataCache) if err != nil { return nil, err } return vulns, nil } // handleOSError handles the common pattern of checking for ErrOSNotPresent func (vp vulnerabilityProvider) handleOSError(err error, osSpecs OSSpecifiers) error { if err == nil { return nil } if errors.Is(err, ErrOSNotPresent) { log.WithFields("os", osSpecs).Debug("no OS found in the DB for the given criteria") return nil } return err } // GetOperatingSystemEOL returns the EOL and EOAS dates for the given distro. // Uses exact matching (no aliasing) since each distro has its own EOL dates // (e.g., CentOS EOL != RHEL EOL even though CentOS aliases to RHEL for vulns). func (vp vulnerabilityProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) { if d == nil { return nil, nil, nil } spec := OSSpecifier{ Name: d.Name(), MajorVersion: d.MajorVersion(), MinorVersion: d.MinorVersion(), RemainingVersion: d.RemainingVersion(), LabelVersion: d.LabelVersion(), DisableAliasing: true, // EOL lookups must use exact distro match DisableFallback: true, // don't fall back to major-only matching (e.g. Alpine 3.24 shouldn't match EOL'd 3.12) } results, err := vp.reader.GetOperatingSystems(spec) if err != nil { if errors.Is(err, ErrMissingOSIdentification) { return nil, nil, nil } return nil, nil, err } if len(results) == 0 { return nil, nil, nil } if len(results) > 1 { log.WithFields("distro", d.String(), "matches", len(results)).Debug("multiple OS records matched for EOL lookup, using first match") } // Return EOL data from the first matching OS record os := results[0] return os.EOLDate, os.EOASDate, nil } // fetchAndProcessCPEs fetches CPEs and returns vulnerabilities directly func (vp vulnerabilityProvider) fetchAndProcessCPEs(query *searchQuery) ([]vulnerability.Vulnerability, error) { var vulns []vulnerability.Vulnerability metadataCache := make(map[string]*vulnerability.Metadata) // only fetch if we have CPE specifications if query.cpeSpec == nil { return vulns, nil } if query.unaffectedOnly { unaffectedCPEs, err := vp.reader.GetUnaffectedCPEs(query.cpeSpec, &GetCPEOptions{ Vulnerabilities: query.vulnSpecs, PreloadBlob: true, }) if err != nil { return nil, err } unaffectedCPEs = filterUnaffectedCPEVersions(query.versionMatcher, unaffectedCPEs, query.cpeSpec) if err = fillUnaffectedCPEHandles(vp.reader, ptrs(unaffectedCPEs)); err != nil { return nil, err } vulns, err = vp.processUnaffectedCPEHandles(vulns, unaffectedCPEs, metadataCache) if err != nil { return nil, err } } else { affectedCPEs, err := vp.reader.GetAffectedCPEs(query.cpeSpec, &GetCPEOptions{ Vulnerabilities: query.vulnSpecs, PreloadBlob: true, }) if err != nil { return nil, err } affectedCPEs = filterAffectedCPEVersions(query.versionMatcher, affectedCPEs, query.cpeSpec) if err = fillAffectedCPEHandles(vp.reader, ptrs(affectedCPEs)); err != nil { return nil, err } vulns, err = vp.processAffectedCPEHandles(vulns, affectedCPEs, metadataCache) if err != nil { return nil, err } } return vulns, nil } func (vp vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulnerability, criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { isMatch := func(v vulnerability.Vulnerability) (bool, error) { for _, c := range criteria { if _, ok := c.(search.VersionConstraintMatcher); ok { continue // already run } matches, reason, err := c.MatchesVulnerability(v) if !matches || err != nil { fields := logger.Fields{ "vulnerability": v, } if err != nil { fields["error"] = err } logDroppedVulnerability(v.ID, reason, fields) return false, err } } return true, nil } for i := 0; i < len(vulns); i++ { matches, err := isMatch(vulns[i]) if err != nil { return nil, err } if !matches { vulns = append(vulns[0:i], vulns[i+1:]...) i-- } } return vulns, nil } // processAffectedPackageHandles processes affected package handles and adds them to the vulnerabilities list func (vp vulnerabilityProvider) processAffectedPackageHandles(out []vulnerability.Vulnerability, handles []AffectedPackageHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) { for _, ph := range handles { if ph.BlobValue == nil { log.Debugf("unable to find blobValue for %+v", ph) continue } v, err := newVulnerabilityFromAffectedPackageHandle(ph, ph.BlobValue.Ranges) if err != nil { return nil, err } if v == nil { continue } meta, err := vp.getCachedMetadata(ph.Vulnerability, v.Namespace, metadataCache) if err != nil { log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability") } else { v.Metadata = meta } out = append(out, *v) } return out, nil } // processAffectedCPEHandles processes affected CPE handles and adds them to the vulnerabilities list func (vp vulnerabilityProvider) processAffectedCPEHandles(out []vulnerability.Vulnerability, handles []AffectedCPEHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) { for _, ch := range handles { if ch.BlobValue == nil { log.Debugf("unable to find blobValue for %+v", ch) continue } v, err := newVulnerabilityFromAffectedCPEHandle(ch, ch.BlobValue.Ranges) if err != nil { return nil, err } if v == nil { continue } meta, err := vp.getCachedMetadata(ch.Vulnerability, v.Namespace, metadataCache) if err != nil { log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability") } else { v.Metadata = meta } out = append(out, *v) } return out, nil } // processUnaffectedPackageHandles processes unaffected package handles and adds them to the vulnerabilities list func (vp vulnerabilityProvider) processUnaffectedPackageHandles(out []vulnerability.Vulnerability, handles []UnaffectedPackageHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) { for _, ph := range handles { if ph.BlobValue == nil { log.Debugf("unable to find blobValue for %+v", ph) continue } v, err := newVulnerabilityFromUnaffectedPackageHandle(ph, ph.BlobValue.Ranges) if err != nil { return nil, err } if v == nil { continue } meta, err := vp.getCachedMetadata(ph.Vulnerability, v.Namespace, metadataCache) if err != nil { log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability") } else { v.Metadata = meta } out = append(out, *v) } return out, nil } // processUnaffectedCPEHandles processes unaffected CPE handles and adds them to the vulnerabilities list func (vp vulnerabilityProvider) processUnaffectedCPEHandles(out []vulnerability.Vulnerability, handles []UnaffectedCPEHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) { for _, ch := range handles { if ch.BlobValue == nil { log.Debugf("unable to find blobValue for %+v", ch) continue } v, err := newVulnerabilityFromUnaffectedCPEHandle(ch, ch.BlobValue.Ranges) if err != nil { return nil, err } if v == nil { continue } meta, err := vp.getCachedMetadata(ch.Vulnerability, v.Namespace, metadataCache) if err != nil { log.WithFields("error", err, "vulnerability", v.String()).Debug("unable to fetch metadata for vulnerability") } else { v.Metadata = meta } out = append(out, *v) } return out, nil } // getCachedMetadata retrieves metadata from cache or fetches it if not cached func (vp vulnerabilityProvider) getCachedMetadata(vuln *VulnerabilityHandle, namespace string, metadataCache map[string]*vulnerability.Metadata) (*vulnerability.Metadata, error) { if vuln == nil { return nil, nil } if metadata, ok := metadataCache[vuln.Name]; ok { return metadata, nil } metadata, err := vp.getVulnerabilityMetadata(vuln, namespace) if err != nil { return nil, err } metadataCache[vuln.Name] = metadata return metadata, nil } func filterAffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []AffectedPackageHandle) []AffectedPackageHandle { // no constraint matcher, just return all packages if constraintMatcher == nil { return packages } var out []AffectedPackageHandle for packageIdx := 0; packageIdx < len(packages); packageIdx++ { handle := packages[packageIdx] vuln := handle.vulnerability() allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue) if !allDropped { out = append(out, handle) continue // keep this handle } reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", ")) f := make(logger.Fields) if handle.Package != nil { f["package"] = handle.Package.String() } else { f["affectedPackage"] = handle } logDroppedVulnerability(vuln, reason, f) } return out } func filterUnaffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []UnaffectedPackageHandle) []UnaffectedPackageHandle { // no constraint matcher, just return all packages if constraintMatcher == nil { return packages } var out []UnaffectedPackageHandle for packageIdx := 0; packageIdx < len(packages); packageIdx++ { handle := packages[packageIdx] vuln := handle.vulnerability() pkgHandle := handle.getPackageHandle() allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, pkgHandle.BlobValue) if !allDropped { out = append(out, handle) continue // keep this handle } reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", ")) f := make(logger.Fields) if pkgHandle.Package != nil { f["package"] = pkgHandle.Package.String() } else { f["unaffectedPackage"] = handle } logDroppedVulnerability(vuln, reason, f) } return out } func filterAffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []AffectedCPEHandle, cpeSpec *cpe.Attributes) []AffectedCPEHandle { // no constraint matcher, just return all packages if constraintMatcher == nil { return handles } var out []AffectedCPEHandle for i := range handles { handle := handles[i] vuln := handle.vulnerability() allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue) if !allDropped { out = append(out, handle) continue // keep this handle } reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", ")) logDroppedVulnerability(vuln, reason, logger.Fields{ "cpe": cpeSpec.String(), }) } return out } func filterUnaffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []UnaffectedCPEHandle, cpeSpec *cpe.Attributes) []UnaffectedCPEHandle { // no constraint matcher, just return all packages if constraintMatcher == nil { return handles } var out []UnaffectedCPEHandle for i := range handles { handle := handles[i] vuln := handle.vulnerability() allDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue) if !allDropped { out = append(out, handle) continue // keep this handle } reason := fmt.Sprintf("not within vulnerability version constraints: %q", strings.Join(unmatchedConstraints, ", ")) logDroppedVulnerability(vuln, reason, logger.Fields{ "cpe": cpeSpec.String(), }) } return out } // filterAffectedPackageRanges returns true if all ranges removed func filterAffectedPackageRanges(matcher search.VersionConstraintMatcher, b *PackageBlob) (bool, []string) { if len(b.Ranges) == 0 { // no ranges means that we're implicitly vulnerable to all versions return false, nil } var unmatchedConstraints []string for _, r := range b.Ranges { v := r.Version format := version.ParseFormat(v.Type) constraint, err := version.GetConstraint(v.Constraint, format) if err != nil || constraint == nil { log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("unable to parse constraint") continue } matches, err := matcher.MatchesConstraint(constraint) if err != nil { log.WithFields("error", err, "constraint", v.Constraint, "format", v.Type).Debug("match constraint error") } if matches { continue } unmatchedConstraints = append(unmatchedConstraints, v.Constraint) } return len(b.Ranges) == len(unmatchedConstraints), unmatchedConstraints } func toSeverityString(sev vulnerability.Severity) string { return strcase.ToCamel(sev.String()) } // returns the first reference url to populate the DataSource func firstReferenceURL(vuln *VulnerabilityHandle) string { for _, v := range vuln.BlobValue.References { return v.URL } return "" } // skip the first reference URL and return the remainder to populate the URLs func lastReferenceURLs(vuln *VulnerabilityHandle) []string { var out []string for i, v := range vuln.BlobValue.References { if i == 0 { continue } out = append(out, v.URL) } return out } func getCVEs(vuln *VulnerabilityHandle) []string { var cves []string set := strset.New() addCVE := func(id string) { lower := strings.ToLower(id) if strings.HasPrefix(lower, "cve-") { if !set.Has(lower) { cves = append(cves, id) set.Add(lower) } } } if vuln == nil { return cves } addCVE(vuln.Name) if vuln.BlobValue == nil { return cves } addCVE(vuln.BlobValue.ID) for _, alias := range vuln.BlobValue.Aliases { addCVE(alias) } return cves } ================================================ FILE: grype/db/v6/vulnerability_provider_mocks_test.go ================================================ package v6 import ( "encoding/hex" "testing" "time" "github.com/stretchr/testify/require" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/namespace" distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/db/v5/namespace/language" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) func testVulnerabilityProvider(t *testing.T) vulnerability.Provider { t.Helper() tmp := t.TempDir() w, err := NewWriter(Config{ DBDirPath: tmp, }) defer log.CloseAndLogError(w, tmp) require.NoError(t, err) aDayAgo := time.Now().Add(-1 * 24 * time.Hour) aWeekAgo := time.Now().Add(-7 * 24 * time.Hour) twoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour) debianProvider := &Provider{ ID: "debian", Version: "1", Processor: "debian-processor", DateCaptured: &aDayAgo, InputDigest: hex.EncodeToString([]byte("debian")), } nvdProvider := &Provider{ ID: "nvd", Version: "1", Processor: "nvd-processor", DateCaptured: &aDayAgo, InputDigest: hex.EncodeToString([]byte("nvd")), } v5vulns := []v5.Vulnerability{ // neutron { PackageName: "neutron", Namespace: "debian:distro:debian:8", VersionConstraint: "< 2014.1.3-6", ID: "CVE-2014-fake-1", VersionFormat: "deb", }, { PackageName: "neutron", Namespace: "debian:distro:debian:8", VersionConstraint: "< 2013.0.2-1", ID: "CVE-2013-fake-2", VersionFormat: "deb", }, // poison the well! this is not a valid entry, but we want the matching process to survive and find other good results... { PackageName: "neutron", Namespace: "debian:distro:debian:8", VersionConstraint: "< 70.3.0-rc0", // intentionally bad value ID: "CVE-2014-fake-3", VersionFormat: "apk", }, // activerecord { PackageName: "activerecord", Namespace: "nvd:cpe", VersionConstraint: "< 3.7.6", ID: "CVE-2014-fake-3", VersionFormat: "unknown", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, { PackageName: "activerecord", Namespace: "nvd:cpe", VersionConstraint: "< 3.7.4", ID: "CVE-2014-fake-4", VersionFormat: "unknown", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", }, }, { PackageName: "activerecord", Namespace: "nvd:cpe", VersionConstraint: "= 4.0.1", ID: "CVE-2014-fake-5", VersionFormat: "unknown", CPEs: []string{ "cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", // shouldn't match on this }, }, { PackageName: "activerecord", Namespace: "nvd:cpe", VersionConstraint: "< 98SP3", ID: "CVE-2014-fake-6", VersionFormat: "unknown", CPEs: []string{ "cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", // shouldn't match on this }, }, { PackageName: "Newtonsoft.Json", Namespace: "github:language:dotnet", ID: "GHSA-5crp-9r3c-p9vr", VersionFormat: "unknown", VersionConstraint: "<13.0.1", }, // poison the well! this is not a valid entry, but we want the matching process to survive and find other good results... { PackageName: "activerecord", Namespace: "nvd:cpe", VersionConstraint: "< 70.3.0-rc0", // intentionally bad value ID: "CVE-2014-fake-7", VersionFormat: "apk", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, } for _, v := range v5vulns { var os *OperatingSystem prov := nvdProvider switch v.Namespace { case "nvd:cpe": case "debian:distro:debian:8": prov = debianProvider os = &OperatingSystem{ Name: "debian", MajorVersion: "8", } } vuln := &VulnerabilityHandle{ ID: 0, Name: v.ID, Status: "", PublishedDate: &twoWeeksAgo, ModifiedDate: &aWeekAgo, WithdrawnDate: nil, ProviderID: prov.ID, Provider: prov, BlobID: 0, BlobValue: &VulnerabilityBlob{ ID: v.ID, Assigners: []string{v.ID + "-assigner-1", v.ID + "-assigner-2"}, Description: v.ID + "-description", References: []Reference{ { URL: "http://somewhere/" + v.ID, Tags: []string{v.ID + "-tag-1", v.ID + "-tag-2"}, }, }, //Aliases: []string{"GHSA-" + v.ID}, Severities: []Severity{ { Scheme: SeveritySchemeCVSS, Value: "high", Source: "", Rank: 0, }, }, }, } err = w.AddVulnerabilities(vuln) require.NoError(t, err) var cpes []Cpe for _, c := range v.CPEs { cp, err := cpe.New(c, "") require.NoError(t, err) cpes = append(cpes, Cpe{ Part: cp.Attributes.Part, Vendor: cp.Attributes.Vendor, Product: cp.Attributes.Product, Edition: cp.Attributes.Edition, Language: cp.Attributes.Language, SoftwareEdition: cp.Attributes.SWEdition, TargetHardware: cp.Attributes.TargetHW, TargetSoftware: cp.Attributes.TargetSW, Other: cp.Attributes.Other, }) } packageType := "" ns, err := namespace.FromString(v.Namespace) require.NoError(t, err) d, _ := ns.(*distroNs.Namespace) if d != nil { packageType = string(d.DistroType()) } lang, _ := ns.(*language.Namespace) if lang != nil { packageType = string(lang.Language()) } pkg := &Package{ ID: 0, Ecosystem: packageType, Name: v.PackageName, //CPEs: cpes, } ap := &AffectedPackageHandle{ ID: 0, VulnerabilityID: 0, Vulnerability: vuln, OperatingSystemID: nil, OperatingSystem: os, PackageID: 0, Package: pkg, BlobID: 0, BlobValue: &PackageBlob{ CVEs: nil, Qualifiers: nil, Ranges: []Range{ { Fix: nil, Version: Version{ Type: v.VersionFormat, Constraint: v.VersionConstraint, }, }, }, }, } err = w.AddAffectedPackages(ap) require.NoError(t, err) for _, c := range cpes { ac := &AffectedCPEHandle{ Vulnerability: vuln, CPE: &c, BlobValue: &PackageBlob{ Ranges: []Range{ { Version: Version{ Type: v.VersionFormat, Constraint: v.VersionConstraint, }, }, }, }, } err = w.AddAffectedCPEs(ac) require.NoError(t, err) } } // add unaffected packages for testing unaffected stores unaffectedVuln := &VulnerabilityHandle{ ID: 0, Name: "CVE-2024-unaffected-test", Status: "", PublishedDate: &twoWeeksAgo, ModifiedDate: &aWeekAgo, WithdrawnDate: nil, ProviderID: nvdProvider.ID, Provider: nvdProvider, BlobID: 0, BlobValue: &VulnerabilityBlob{ ID: "CVE-2024-unaffected-test", Assigners: []string{"CVE-2024-unaffected-test-assigner-1"}, Description: "CVE-2024-unaffected-test-description", References: []Reference{ { URL: "http://somewhere/CVE-2024-unaffected-test", Tags: []string{"CVE-2024-unaffected-test-tag-1"}, }, }, Severities: []Severity{ { Scheme: SeveritySchemeCVSS, Value: "medium", Source: "", Rank: 0, }, }, }, } err = w.AddVulnerabilities(unaffectedVuln) require.NoError(t, err) // add unaffected package: test-unaffected-package testUnaffectedPkg := &Package{ ID: 0, Ecosystem: "deb", Name: "test-unaffected-package", } testUnaffectedPackageHandle := &UnaffectedPackageHandle{ ID: 0, VulnerabilityID: 0, Vulnerability: unaffectedVuln, OperatingSystemID: nil, OperatingSystem: nil, PackageID: 0, Package: testUnaffectedPkg, BlobID: 0, BlobValue: &PackageBlob{ CVEs: nil, Qualifiers: nil, Ranges: []Range{ { Fix: nil, Version: Version{ Type: "deb", Constraint: "< 1.0.0", }, }, }, }, } err = w.AddUnaffectedPackages(testUnaffectedPackageHandle) require.NoError(t, err) // add unaffected CPE testUnaffectedCPE, err := cpe.New("cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*", "") require.NoError(t, err) testUnaffectedCPEModel := Cpe{ Part: testUnaffectedCPE.Attributes.Part, Vendor: testUnaffectedCPE.Attributes.Vendor, Product: testUnaffectedCPE.Attributes.Product, Edition: testUnaffectedCPE.Attributes.Edition, Language: testUnaffectedCPE.Attributes.Language, SoftwareEdition: testUnaffectedCPE.Attributes.SWEdition, TargetHardware: testUnaffectedCPE.Attributes.TargetHW, TargetSoftware: testUnaffectedCPE.Attributes.TargetSW, Other: testUnaffectedCPE.Attributes.Other, } testUnaffectedCPEHandle := &UnaffectedCPEHandle{ ID: 0, VulnerabilityID: 0, Vulnerability: unaffectedVuln, CpeID: 0, CPE: &testUnaffectedCPEModel, BlobID: 0, BlobValue: &PackageBlob{ CVEs: nil, Qualifiers: nil, Ranges: []Range{ { Fix: nil, Version: Version{ Type: "unknown", Constraint: "< 1.0.0", }, }, }, }, } err = w.AddUnaffectedCPEs(testUnaffectedCPEHandle) require.NoError(t, err) // add unaffected neutron package for distro test neutronUnaffectedPkg := &Package{ ID: 0, Ecosystem: "deb", Name: "neutron", } neutronUnaffectedHandle := &UnaffectedPackageHandle{ ID: 0, VulnerabilityID: 0, Vulnerability: unaffectedVuln, OperatingSystemID: nil, OperatingSystem: &OperatingSystem{ Name: "debian", MajorVersion: "8", }, PackageID: 0, Package: neutronUnaffectedPkg, BlobID: 0, BlobValue: &PackageBlob{ CVEs: nil, Qualifiers: nil, Ranges: []Range{ { Fix: nil, Version: Version{ Type: "deb", Constraint: ">= 2015.0.0", }, }, }, }, } err = w.AddUnaffectedPackages(neutronUnaffectedHandle) require.NoError(t, err) return NewVulnerabilityProvider(setupReadOnlyTestStore(t, tmp)) } ================================================ FILE: grype/db/v6/vulnerability_provider_test.go ================================================ package v6 import ( "errors" "testing" "unicode" "unicode/utf8" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func Test_FindVulnerabilitiesByDistro(t *testing.T) { provider := testVulnerabilityProvider(t) d := distro.New(distro.Debian, "8", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "1.0.0", Type: syftPkg.DebPkg, } actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name), search.ByVersion(*version.New(p.Version, pkg.VersionFormat(p)))) require.NoError(t, err) expected := []vulnerability.Vulnerability{ { PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: nil, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-1", DataSource: "http://somewhere/CVE-2014-fake-1", Namespace: "debian:distro:debian:8", Severity: "High", URLs: nil, Description: "CVE-2014-fake-1-description", }, RelatedVulnerabilities: []vulnerability.Reference{{ID: "CVE-2014-fake-1", Namespace: "nvd:cpe"}}, }, { PackageName: "neutron", Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2013-fake-2", Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: nil, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2013-fake-2", DataSource: "http://somewhere/CVE-2013-fake-2", Namespace: "debian:distro:debian:8", Severity: "High", URLs: nil, Description: "CVE-2013-fake-2-description", }, RelatedVulnerabilities: []vulnerability.Reference{{ID: "CVE-2013-fake-2", Namespace: "nvd:cpe"}}, }, } require.Len(t, actual, len(expected)) for idx, vuln := range actual { if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" { t.Errorf("diff: %+v", d) } } } func Test_FindVulnerabilitiesByEmptyDistro(t *testing.T) { provider := testVulnerabilityProvider(t) p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", } vulnerabilities, err := provider.FindVulnerabilities(search.ByDistro(distro.Distro{}), search.ByPackageName(p.Name)) require.Empty(t, vulnerabilities) require.NoError(t, err) } func Test_FindVulnerabilitiesByCPE(t *testing.T) { tests := []struct { name string cpe cpe.CPE expected []vulnerability.Vulnerability err bool }{ { name: "match from name and target SW", cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", ""), expected: []vulnerability.Vulnerability{ { PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-4", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-4", DataSource: "http://somewhere/CVE-2014-fake-4", Namespace: "nvd:cpe", Severity: "High", URLs: nil, Description: "CVE-2014-fake-4-description", }, }, }, }, { name: "match with normalization", cpe: cpe.Must("cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*", ""), expected: []vulnerability.Vulnerability{ { PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-4", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-4", DataSource: "http://somewhere/CVE-2014-fake-4", Namespace: "nvd:cpe", Severity: "High", URLs: nil, Description: "CVE-2014-fake-4-description", }, }, }, }, { name: "match from vendor & name", cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*", ""), expected: []vulnerability.Vulnerability{ { PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-3", DataSource: "http://somewhere/CVE-2014-fake-3", Namespace: "nvd:cpe", Severity: "High", URLs: nil, Description: "CVE-2014-fake-3-description", }, }, { PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-4", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-4", DataSource: "http://somewhere/CVE-2014-fake-4", Namespace: "nvd:cpe", Severity: "High", URLs: nil, Description: "CVE-2014-fake-4-description", }, }, { PackageName: "activerecord", Constraint: version.MustGetConstraint("< 70.3.0-rc0", version.ApkFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-7", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-7", DataSource: "http://somewhere/CVE-2014-fake-7", Namespace: "nvd:cpe", Severity: "High", URLs: nil, Description: "CVE-2014-fake-7-description", }, }, }, }, { name: "allow query with only product", cpe: cpe.Must("cpe:2.3:a:*:product:*:*:*:*:*:*:*:*", ""), }, { name: "do not allow query without product", cpe: cpe.CPE{ Attributes: cpe.Attributes{ Part: "a", Vendor: "v", }, }, err: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { provider := testVulnerabilityProvider(t) actual, err := provider.FindVulnerabilities(search.ByCPE(test.cpe)) if err != nil && !test.err { t.Fatalf("expected no err, got: %+v", err) } else if err == nil && test.err { t.Fatalf("expected an err, gots" + " none") } require.Len(t, actual, len(test.expected)) for idx, vuln := range actual { if d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != "" { t.Errorf("diff: %+v", d) } } }) } } func Test_FindVulnerabilitiesByID(t *testing.T) { provider := testVulnerabilityProvider(t) d := distro.New(distro.Debian, "8", "") // with distro actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-1")) require.NoError(t, err) expected := []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), PackageQualifiers: []qualifier.Qualifier{}, CPEs: nil, Advisories: []vulnerability.Advisory{}, Metadata: &vulnerability.Metadata{ ID: "CVE-2014-fake-1", DataSource: "http://somewhere/CVE-2014-fake-1", Namespace: "debian:distro:debian:8", Severity: "High", URLs: nil, Description: "CVE-2014-fake-1-description", }, RelatedVulnerabilities: []vulnerability.Reference{{ID: "CVE-2014-fake-1", Namespace: "nvd:cpe"}}, }, } require.Len(t, actual, len(expected)) for idx, vuln := range actual { if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" { t.Errorf("diff: %+v", d) } } // without distro actual, err = provider.FindVulnerabilities(search.ByID("CVE-2014-fake-1")) require.NoError(t, err) for idx, vuln := range actual { if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" { t.Errorf("diff: %+v", d) } } } func Test_FindVulnerabilitiesByEcosystem_UnknownPackageType(t *testing.T) { tests := []struct { name string packageName string packageType syftPkg.Type language syftPkg.Language expectedIDs []string }{ { name: "known package type", packageName: "Newtonsoft.Json", packageType: syftPkg.DotnetPkg, language: syftPkg.Java, // deliberately wrong to prove we're using package type expectedIDs: []string{"GHSA-5crp-9r3c-p9vr"}, }, { name: "unknown package type, known language", packageName: "Newtonsoft.Json", packageType: syftPkg.UnknownPkg, language: syftPkg.Dotnet, expectedIDs: []string{"GHSA-5crp-9r3c-p9vr"}, }, { name: "unknown package type, unknown language", packageName: "Newtonsoft.Json", packageType: syftPkg.UnknownPkg, language: syftPkg.UnknownLanguage, // The vuln GHSA-5crp-9r3c-p9vr is specifically associated // with the dotnet ecosystem, so it should not be returned here. // In a real search for UnknownPkg + UnknownLanguage, there should // be a separate search.ByCPE run that _does_ return it. expectedIDs: []string{}, }, } provider := testVulnerabilityProvider(t) for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := provider.FindVulnerabilities( search.ByEcosystem(test.language, test.packageType), search.ByPackageName(test.packageName), ) require.NoError(t, err) actualIDs := make([]string, len(actual)) for idx, vuln := range actual { actualIDs[idx] = vuln.ID } if d := cmp.Diff(test.expectedIDs, actualIDs); d != "" { t.Errorf("diff: %+v", d) } }) } } func Test_DataSource(t *testing.T) { tests := []struct { name string vuln VulnerabilityHandle expected vulnerability.Metadata }{ { name: "no reference urls", vuln: VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ References: nil, }, }, expected: vulnerability.Metadata{ DataSource: "", URLs: nil, }, }, { name: "one reference url", vuln: VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ References: []Reference{ { URL: "url1", }, }, }, }, expected: vulnerability.Metadata{ DataSource: "url1", URLs: nil, }, }, { name: "two reference urls", vuln: VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ References: []Reference{ { URL: "url1", }, { URL: "url2", }, }, }, }, expected: vulnerability.Metadata{ DataSource: "url1", URLs: []string{"url2"}, }, }, { name: "many reference urls", vuln: VulnerabilityHandle{ BlobValue: &VulnerabilityBlob{ References: []Reference{ { URL: "url4", }, { URL: "url3", }, { URL: "url2", }, { URL: "url1", }, }, }, }, expected: vulnerability.Metadata{ DataSource: "url4", URLs: []string{"url3", "url2", "url1"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := newVulnerabilityMetadata(&tt.vuln, "", nil, nil, nil) got.Severity = "" require.NoError(t, err) if diff := cmp.Diff(&tt.expected, got, cmpOpts()...); diff != "" { t.Fatal(diff) } }) } } func Test_filterAffectedPackageRanges(t *testing.T) { tests := []struct { name string ranges []Range matchesConstraint func(constraint version.Constraint) (bool, error) expectedAllRangesRemoved bool expectedUnmatchedStrings []string }{ { name: "no ranges", ranges: nil, expectedAllRangesRemoved: false, // important! we assume that a vulnerability with no ranges is always vulnerable expectedUnmatchedStrings: nil, }, { name: "has ranges within constraint", ranges: []Range{ { Version: Version{ Type: "rpm", Constraint: "< 1.0.0", }, }, { Version: Version{ Type: "rpm", Constraint: "< 2.0.0", }, }, }, matchesConstraint: func(constraint version.Constraint) (bool, error) { return true, nil }, expectedAllRangesRemoved: false, expectedUnmatchedStrings: nil, }, { name: "has ranges outside constraint", ranges: []Range{ { Version: Version{ Type: "rpm", Constraint: "< 1.0.0", }, }, { Version: Version{ Type: "rpm", Constraint: "< 2.0.0", }, }, }, matchesConstraint: func(constraint version.Constraint) (bool, error) { return false, nil }, expectedAllRangesRemoved: true, expectedUnmatchedStrings: []string{"< 1.0.0", "< 2.0.0"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockMatcher := &mockVersionConstraintMatcher{ matchesConstraintFunc: tt.matchesConstraint, } blob := &PackageBlob{ Ranges: tt.ranges, } allRangesRemoved, unmatchedConstraints := filterAffectedPackageRanges(mockMatcher, blob) require.Equal(t, tt.expectedAllRangesRemoved, allRangesRemoved) require.Equal(t, tt.expectedUnmatchedStrings, unmatchedConstraints) }) } } type mockVersionConstraintMatcher struct { matchesConstraintFunc func(constraint version.Constraint) (bool, error) } func (m *mockVersionConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) { if m.matchesConstraintFunc != nil { return m.matchesConstraintFunc(constraint) } return false, nil } func Test_FindVulnerabilitiesByUnaffectedCriteria(t *testing.T) { provider := testVulnerabilityProvider(t) tests := []struct { name string criteria []vulnerability.Criteria expected []vulnerability.Vulnerability description string }{ { name: "search for unaffected packages", criteria: []vulnerability.Criteria{ search.ForUnaffected(), search.ByPackageName("test-unaffected-package"), }, expected: []vulnerability.Vulnerability{ { PackageName: "test-unaffected-package", Constraint: version.MustGetConstraint("< 1.0.0", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2024-unaffected-test", Namespace: "nvd:language:deb", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: nil, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: nil, Metadata: &vulnerability.Metadata{ ID: "CVE-2024-unaffected-test", DataSource: "http://somewhere/CVE-2024-unaffected-test", Namespace: "nvd:language:deb", Severity: "Medium", URLs: nil, Description: "CVE-2024-unaffected-test-description", }, Unaffected: true, }, }, description: "should use unaffected package store", }, { name: "search for unaffected CPEs", criteria: []vulnerability.Criteria{ search.ForUnaffected(), search.ByCPE(cpe.Must("cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*", "")), }, expected: []vulnerability.Vulnerability{ { PackageName: "unaffected", Constraint: version.MustGetConstraint("< 1.0.0", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2024-unaffected-test", Namespace: "nvd:cpe", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*", "")}, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: nil, Metadata: &vulnerability.Metadata{ ID: "CVE-2024-unaffected-test", DataSource: "http://somewhere/CVE-2024-unaffected-test", Namespace: "nvd:cpe", Severity: "Medium", URLs: nil, Description: "CVE-2024-unaffected-test-description", }, Unaffected: true, }, }, description: "should use unaffected CPE store", }, { name: "search with unaffected criteria and distro", criteria: []vulnerability.Criteria{ search.ForUnaffected(), search.ByDistro(*distro.New(distro.Debian, "8", "")), search.ByPackageName("neutron"), }, expected: []vulnerability.Vulnerability{ { PackageName: "neutron", Constraint: version.MustGetConstraint(">= 2015.0.0", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2024-unaffected-test", Namespace: "nvd:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: nil, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: nil, Metadata: &vulnerability.Metadata{ ID: "CVE-2024-unaffected-test", DataSource: "http://somewhere/CVE-2024-unaffected-test", Namespace: "nvd:distro:debian:8", Severity: "Medium", URLs: nil, Description: "CVE-2024-unaffected-test-description", }, Unaffected: true, }, }, description: "should combine unaffected and distro criteria", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := provider.FindVulnerabilities(test.criteria...) require.NoError(t, err, test.description) require.Len(t, actual, len(test.expected), test.description) for idx, vuln := range actual { if d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != "" { t.Errorf("diff: %+v", d) } } }) } } func Test_FindVulnerabilitiesErrorHandling(t *testing.T) { provider := testVulnerabilityProvider(t) tests := []struct { name string criteria []vulnerability.Criteria expectError bool description string }{ { name: "CPE without product should error", criteria: []vulnerability.Criteria{ search.ByCPE(cpe.CPE{ Attributes: cpe.Attributes{ Part: "a", Vendor: "vendor", // Product is missing - should cause error }, }), }, expectError: true, description: "CPE searches require a product", }, { name: "empty criteria should work", criteria: []vulnerability.Criteria{}, expectError: false, description: "empty criteria should be handled gracefully", }, { name: "unaffected with CPE without product should error", criteria: []vulnerability.Criteria{ search.ForUnaffected(), search.ByCPE(cpe.CPE{ Attributes: cpe.Attributes{ Part: "a", Vendor: "vendor", }, }), }, expectError: true, description: "unaffected CPE searches also require a product", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := provider.FindVulnerabilities(test.criteria...) if test.expectError { require.Error(t, err, test.description) } else { require.NoError(t, err, test.description) } }) } } func Test_FindVulnerabilitiesMixedCriteria(t *testing.T) { provider := testVulnerabilityProvider(t) // test complex criteria combinations d := distro.New(distro.Debian, "8", "") tests := []struct { name string criteria []vulnerability.Criteria expectedLen int description string }{ { name: "package name with version constraint", criteria: []vulnerability.Criteria{ search.ByDistro(*d), search.ByPackageName("neutron"), search.ByVersion(*version.New("1.0.0", version.DebFormat)), }, expectedLen: 2, // based on existing test data description: "should find vulnerabilities matching version constraints", }, { name: "ID search with distro", criteria: []vulnerability.Criteria{ search.ByDistro(*d), search.ByID("CVE-2014-fake-1"), }, expectedLen: 1, description: "should find specific vulnerability by ID and distro", }, { name: "ecosystem search with package name", criteria: []vulnerability.Criteria{ search.ByEcosystem(syftPkg.Dotnet, syftPkg.DotnetPkg), search.ByPackageName("Newtonsoft.Json"), }, expectedLen: 1, description: "should find vulnerabilities by ecosystem", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := provider.FindVulnerabilities(test.criteria...) require.NoError(t, err, test.description) require.Len(t, actual, test.expectedLen, test.description) }) } } func Test_FindVulnerabilitiesNormalization(t *testing.T) { provider := testVulnerabilityProvider(t) tests := []struct { name string packageName string packageType syftPkg.Type expectedResults int description string }{ { name: "package name normalization with known type", packageName: "Newtonsoft.Json", // mixed case packageType: syftPkg.DotnetPkg, expectedResults: 1, description: "package names should be normalized based on ecosystem", }, { name: "package name with unknown type", packageName: "SomePackage", packageType: syftPkg.UnknownPkg, expectedResults: 0, // likely no matches description: "unknown package types should still be handled", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := provider.FindVulnerabilities( search.ByEcosystem(syftPkg.UnknownLanguage, test.packageType), search.ByPackageName(test.packageName), ) require.NoError(t, err, test.description) require.Len(t, actual, test.expectedResults, test.description) }) } } func Test_HandleOSError(t *testing.T) { provider := testVulnerabilityProvider(t).(*vulnerabilityProvider) osSpecs := OSSpecifiers{&OSSpecifier{Name: "debian", MajorVersion: "8"}} tests := []struct { name string inputError error expectError bool description string }{ { name: "nil error returns nil", inputError: nil, expectError: false, description: "nil errors should pass through as nil", }, { name: "ErrOSNotPresent returns nil", inputError: ErrOSNotPresent, expectError: false, description: "ErrOSNotPresent should be handled and return nil", }, { name: "other errors return unchanged", inputError: errors.New("some other error"), expectError: true, description: "other errors should be returned unchanged", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := provider.handleOSError(test.inputError, osSpecs) if test.expectError { require.Error(t, err, test.description) } else { require.NoError(t, err, test.description) } }) } } func Test_FetchAndProcessPackagesEdgeCases(t *testing.T) { provider := testVulnerabilityProvider(t).(*vulnerabilityProvider) tests := []struct { name string ctx *searchQuery description string }{ { name: "context with no specs should return empty results", ctx: &searchQuery{ // no pkgSpec, no vulnSpecs osSpecs: OSSpecifiers{NoOSSpecified}, unaffectedOnly: false, }, description: "should handle context with no package or vulnerability specs", }, { name: "unaffected context with no specs should return empty results", ctx: &searchQuery{ // no pkgSpec, no vulnSpecs osSpecs: OSSpecifiers{NoOSSpecified}, unaffectedOnly: true, }, description: "should handle unaffected context with no package or vulnerability specs", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { vulns, err := provider.fetchAndProcessPackages(test.ctx) require.NoError(t, err, test.description) require.Empty(t, vulns, "should return empty vulnerabilities") }) } } func Test_FetchAndProcessCPEsEdgeCases(t *testing.T) { provider := testVulnerabilityProvider(t).(*vulnerabilityProvider) tests := []struct { name string ctx *searchQuery description string }{ { name: "context with no CPE spec should return empty results", ctx: &searchQuery{ // no cpeSpec osSpecs: OSSpecifiers{NoOSSpecified}, unaffectedOnly: false, }, description: "should handle context with no CPE spec", }, { name: "unaffected context with no CPE spec should return empty results", ctx: &searchQuery{ // no cpeSpec osSpecs: OSSpecifiers{NoOSSpecified}, unaffectedOnly: true, }, description: "should handle unaffected context with no CPE spec", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { vulns, err := provider.fetchAndProcessCPEs(test.ctx) require.NoError(t, err, test.description) require.Empty(t, vulns, "should return empty vulnerabilities") }) } } func cmpOpts() []cmp.Option { return []cmp.Option{ // globally ignore unexported -- these are unexported structs we cannot reference here to use cmpopts.IgnoreUnexported cmp.FilterPath(func(p cmp.Path) bool { sf, ok := p.Index(-1).(cmp.StructField) if !ok { return false } r, _ := utf8.DecodeRuneInString(sf.Name()) return !unicode.IsUpper(r) }, cmp.Ignore()), cmpopts.EquateEmpty(), cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"), } } ================================================ FILE: grype/db/v6/vulnerability_store.go ================================================ package v6 import ( "fmt" "strings" "time" "github.com/scylladb/go-set/strset" "gorm.io/gorm" "github.com/anchore/go-logger" "github.com/anchore/grype/internal/log" ) const anyVulnerability = "any" type VulnerabilityStoreWriter interface { AddVulnerabilities(vulns ...*VulnerabilityHandle) error } type VulnerabilityStoreReader interface { GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error) } type GetVulnerabilityOptions struct { Preload bool Limit int } type VulnerabilitySpecifiers []VulnerabilitySpecifier type VulnerabilitySpecifier struct { // Name of the vulnerability (e.g. CVE-2020-1234) Name string // ID is the DB ID of the vulnerability ID ID // Status is the status of the vulnerability (e.g. "active", "rejected", etc.) Status VulnerabilityStatus // PublishedAfter is a filter to only return vulnerabilities published after the given time PublishedAfter *time.Time // ModifiedAfter is a filter to only return vulnerabilities modified after the given time ModifiedAfter *time.Time // IncludeAliases for the given name or ID in results IncludeAliases bool // Providers Providers []string } func (v *VulnerabilitySpecifier) String() string { var parts []string if v.Name != "" { parts = append(parts, fmt.Sprintf("name=%s", v.Name)) } if v.ID != 0 { parts = append(parts, fmt.Sprintf("id=%d", v.ID)) } if v.Status != "" { parts = append(parts, fmt.Sprintf("status=%s", v.Status)) } if v.PublishedAfter != nil { parts = append(parts, fmt.Sprintf("publishedAfter=%s", v.PublishedAfter.String())) } if v.ModifiedAfter != nil { parts = append(parts, fmt.Sprintf("modifiedAfter=%s", v.ModifiedAfter.String())) } if v.IncludeAliases { parts = append(parts, "includeAliases=true") } if len(v.Providers) > 0 { parts = append(parts, fmt.Sprintf("providers=%s", strings.Join(v.Providers, ","))) } if len(parts) == 0 { return anyVulnerability } return fmt.Sprintf("vulnerability(%s)", strings.Join(parts, ", ")) } func (s VulnerabilitySpecifiers) String() string { if len(s) == 0 { return anyVulnerability } var parts []string for _, v := range s { parts = append(parts, v.String()) } return strings.Join(parts, ", ") } func DefaultGetVulnerabilityOptions() *GetVulnerabilityOptions { return &GetVulnerabilityOptions{ Preload: false, } } type vulnerabilityStore struct { db *gorm.DB blobStore *blobStore } func newVulnerabilityStore(db *gorm.DB, bs *blobStore) *vulnerabilityStore { return &vulnerabilityStore{ db: db, blobStore: bs, } } func (s *vulnerabilityStore) AddVulnerabilities(vulnerabilities ...*VulnerabilityHandle) error { if err := s.addProviders(s.db, vulnerabilities...); err != nil { return fmt.Errorf("unable to add providers: %w", err) } for i := range vulnerabilities { v := vulnerabilities[i] // this adds the blob value to the DB and sets the ID on the vulnerability handle if err := s.blobStore.addBlobable(v); err != nil { return fmt.Errorf("unable to add affected blob: %w", err) } if v.PublishedDate != nil && v.ModifiedDate == nil { // the data here should be consistent, and we are norming around initial publication counts as a modification date. // this allows for easily refining queries based on both publication date and modification date without needing // to worry about this edge case. v.ModifiedDate = v.PublishedDate } if v.BlobValue != nil { aliases := strset.New(v.BlobValue.Aliases...) aliases.Remove(v.Name) var aliasModels []VulnerabilityAlias for _, alias := range aliases.List() { aliasModels = append(aliasModels, VulnerabilityAlias{ Name: v.Name, Alias: alias, }) } for _, aliasModel := range aliasModels { if err := s.db.FirstOrCreate(&aliasModel).Error; err != nil { return err } } } if err := createRecordsWithCache(s.db, v); err != nil { return err } } return nil } func (s *vulnerabilityStore) addProviders(tx *gorm.DB, vulnerabilities ...*VulnerabilityHandle) error { // nolint:dupl cacheInst, ok := cacheFromContext(tx.Statement.Context) if !ok { return fmt.Errorf("unable to fetch provider cache from context") } var final []*Provider byCacheKey := make(map[string][]*Provider) for _, v := range vulnerabilities { if v.Provider != nil { key := v.Provider.cacheKey() if existingID, ok := cacheInst.getString(v.Provider); ok { // seen in a previous transaction... v.ProviderID = existingID } else if _, ok := byCacheKey[key]; !ok { // not seen within this transaction final = append(final, v.Provider) } byCacheKey[key] = append(byCacheKey[key], v.Provider) } } if len(final) == 0 { return nil } if err := tx.Create(final).Error; err != nil { return fmt.Errorf("unable to create provider records: %w", err) } // update the cache with the new records for _, ref := range final { cacheInst.set(ref) } // update all references with the IDs from the cache for _, refs := range byCacheKey { for _, ref := range refs { id, ok := cacheInst.getString(ref) if ok { ref.setRowID(id) } } } // update the parent objects with the FK ID for _, p := range vulnerabilities { if p.Provider != nil { p.ProviderID = p.Provider.ID } } return nil } func createRecordsWithCache(tx *gorm.DB, items ...*VulnerabilityHandle) error { // look for existing records from the cache, and only create new records cacheInst, ok := cacheFromContext(tx.Statement.Context) if !ok { return fmt.Errorf("cache not found in context") } // store all entries by their cache key (throw away duplicates) skippedRecordsByCacheKey := map[string][]*VulnerabilityHandle{} usedKeys := strset.New() var finalWrites []*VulnerabilityHandle for i := range items { p := items[i] key := p.cacheKey() if usedKeys.Has(key) { skippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p) continue } if _, ok := skippedRecordsByCacheKey[key]; ok { skippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p) continue } if _, ok := cacheInst.getID(p); ok { skippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p) continue } finalWrites = append(finalWrites, p) usedKeys.Add(key) } for i := range finalWrites { if err := tx.Omit("Provider").Create(finalWrites[i]).Error; err != nil { return fmt.Errorf("unable to create record %#v: %w", finalWrites[i], err) } } // ensure we're always updating the cache with the latest data + the records with any new IDs for i := range finalWrites { cacheInst.set(finalWrites[i]) } for _, batch := range skippedRecordsByCacheKey { for i := range batch { id, ok := cacheInst.getID(batch[i]) if !ok { return fmt.Errorf("unable to find ID: %#v", batch[i]) } batch[i].setRowID(id) } } return nil } func (s *vulnerabilityStore) GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error) { if config == nil { config = DefaultGetVulnerabilityOptions() } fields := logger.Fields{ "vuln": vuln, "preload": config.Preload, } start := time.Now() var count int defer func() { fields["duration"] = time.Since(start) fields["records"] = count log.WithFields(fields).Trace("fetched vulnerability records") }() var err error query := s.db if vuln != nil { query, err = handleVulnerabilityOptions(s.db, query, *vuln) if err != nil { return nil, err } } query = s.handlePreload(query, *config) var models []VulnerabilityHandle var results []*VulnerabilityHandle if err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error { if config.Preload { var blobs []blobable for _, r := range results { blobs = append(blobs, r) } if err := s.blobStore.attachBlobValue(blobs...); err != nil { return fmt.Errorf("unable to attach vulnerability blobs: %w", err) } } for _, r := range results { models = append(models, *r) } count += len(results) if config.Limit > 0 && len(models) >= config.Limit { return ErrLimitReached } return nil }).Error; err != nil { return models, fmt.Errorf("unable to fetch vulnerability records: %w", err) } return models, err } func (s *vulnerabilityStore) handlePreload(query *gorm.DB, config GetVulnerabilityOptions) *gorm.DB { var limitArgs []interface{} if config.Limit > 0 { query = query.Limit(config.Limit) limitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB { return db.Limit(config.Limit) }) } if config.Preload { query = query.Preload("Provider", limitArgs...) } return query } func handleVulnerabilityOptions(base, parentQuery *gorm.DB, configs ...VulnerabilitySpecifier) (*gorm.DB, error) { if len(configs) == 0 { return parentQuery, nil } orConditions := base.Model(&VulnerabilityHandle{}) var includeAliasJoin bool for _, config := range configs { query := base.Model(&VulnerabilityHandle{}) if config.Name != "" { if config.IncludeAliases { includeAliasJoin = true query = query.Where("vulnerability_handles.name = ? collate nocase OR vulnerability_aliases.alias = ? collate nocase", config.Name, config.Name) } else { query = query.Where("vulnerability_handles.name = ? collate nocase", config.Name) } } if config.ID != 0 { query = query.Where("vulnerability_handles.id = ?", config.ID) } if config.PublishedAfter != nil { query = query.Where("vulnerability_handles.published_date > ?", *config.PublishedAfter) } if config.ModifiedAfter != nil { query = query.Where("vulnerability_handles.modified_date > ?", *config.ModifiedAfter) } if config.Status != "" { query = query.Where("vulnerability_handles.status = ?", config.Status) } if len(config.Providers) > 0 { query = query.Where("vulnerability_handles.provider_id IN ?", config.Providers) } orConditions = orConditions.Or(query) } if includeAliasJoin { parentQuery = parentQuery.Joins("LEFT JOIN vulnerability_aliases ON vulnerability_aliases.name = vulnerability_handles.name collate nocase") } return parentQuery.Where(orConditions), nil } ================================================ FILE: grype/db/v6/vulnerability_store_test.go ================================================ package v6 import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestVulnerabilityStore_AddVulnerabilities(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) vuln1 := VulnerabilityHandle{ Name: "CVE-1234-5678", BlobValue: &VulnerabilityBlob{ ID: "CVE-1234-5678", }, Provider: &Provider{ ID: "provider!", }, } vuln2 := testVulnerabilityHandle() err := s.AddVulnerabilities(&vuln1, &vuln2) require.NoError(t, err) var result1 VulnerabilityHandle err = db.Where("name = ?", "CVE-1234-5678").First(&result1).Error require.NoError(t, err) assert.Equal(t, vuln1.Name, result1.Name) assert.Equal(t, vuln1.ID, result1.ID) assert.Equal(t, vuln1.BlobID, result1.BlobID) assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch assert.Nil(t, result1.Provider) // since we're not preloading any fields on the fetch var result2 VulnerabilityHandle err = db.Where("name = ?", "CVE-8765-4321").First(&result2).Error require.NoError(t, err) assert.Equal(t, vuln2.Name, result2.Name) assert.Equal(t, vuln2.ID, result2.ID) assert.Equal(t, vuln2.BlobID, result2.BlobID) assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch assert.Nil(t, result1.Provider) // since we're not preloading any fields on the fetch } func TestVulnerabilityStore_NoDuplicateVulnerabilities(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) vuln := VulnerabilityHandle{ Name: "CVE-1234-5678", BlobValue: &VulnerabilityBlob{ ID: "CVE-1234-5678", }, Provider: &Provider{ ID: "provider!", }, } err := s.AddVulnerabilities(&vuln) require.NoError(t, err) err = s.AddVulnerabilities(&vuln) require.NoError(t, err) var results []VulnerabilityHandle err = db.Where("name = ?", "CVE-1234-5678").Preload("Provider").Find(&results).Error require.NoError(t, err) require.Len(t, results, 1, "expected exactly one vulnerability handle to be added") result := results[0] assert.NotEmpty(t, result.ProviderID) assert.NotEmpty(t, result.BlobID) if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "BlobValue")); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } func TestVulnerabilityStore_AddVulnerabilities_missingModifiedDate(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) now := time.Now() vuln := &VulnerabilityHandle{ Name: "CVE-1234-5678", PublishedDate: &now, // have publication date without modification date Provider: &Provider{ ID: "provider!", }, } err := s.AddVulnerabilities(vuln) require.NoError(t, err) // patched! assert.NotNil(t, vuln.ModifiedDate) } func TestVulnerabilityStore_AddVulnerabilities_Aliases(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) vuln := &VulnerabilityHandle{ Name: "CVE-1234-5678", BlobValue: &VulnerabilityBlob{ ID: "CVE-1234-5678", Aliases: []string{"ALIAS-1", "ALIAS-2", "CVE-1234-5678"}, }, Provider: &Provider{ ID: "provider!", }, } err := s.AddVulnerabilities(vuln) require.NoError(t, err) var aliases []VulnerabilityAlias err = db.Where("name = ?", "CVE-1234-5678").Find(&aliases).Error require.NoError(t, err) expectedAliases := []VulnerabilityAlias{ {Name: "CVE-1234-5678", Alias: "ALIAS-1"}, {Name: "CVE-1234-5678", Alias: "ALIAS-2"}, } assert.Len(t, aliases, len(expectedAliases)) for _, expected := range expectedAliases { assert.Contains(t, aliases, expected) } uniqueAliases := make(map[string]struct{}) for _, alias := range aliases { key := alias.Name + ":" + alias.Alias _, exists := uniqueAliases[key] assert.False(t, exists, "duplicate alias found") uniqueAliases[key] = struct{}{} } } func TestVulnerabilityStore_GetVulnerability_ByID(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) vuln := testVulnerabilityHandle() err := s.AddVulnerabilities(&vuln) require.NoError(t, err) results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, nil) // don't preload by default require.NoError(t, err) require.Len(t, results, 1) result := results[0] if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "Provider", "BlobValue")); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } assert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch assert.Nil(t, result.Provider) // since we're not preloading any fields on the fetch results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, &GetVulnerabilityOptions{Preload: true}) require.NoError(t, err) require.Len(t, results, 1) result = results[0] assert.NotNil(t, result.BlobValue) assert.NotNil(t, result.Provider) if d := cmp.Diff(vuln, result); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } func TestVulnerabilityStore_GetVulnerabilities_ByName(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) vuln1 := testVulnerabilityHandle() name := vuln1.Name vuln2 := VulnerabilityHandle{Name: name, BlobID: 2, Provider: vuln1.Provider, BlobValue: &VulnerabilityBlob{ ID: name, }} err := s.AddVulnerabilities(&vuln1, &vuln2) require.NoError(t, err) expected := []VulnerabilityHandle{vuln1, vuln2} results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, nil) // don't preload by default require.NoError(t, err) require.Len(t, results, 2) for i, result := range results { assert.Equal(t, expected[i].Name, result.Name) assert.Equal(t, expected[i].ID, result.ID) assert.Equal(t, expected[i].BlobID, result.BlobID) assert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch assert.Nil(t, result.Provider) // since we're not preloading any fields on the fetch } results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, &GetVulnerabilityOptions{Preload: true}) require.NoError(t, err) require.Len(t, results, 2) for i, result := range results { if d := cmp.Diff(expected[i], result); d != "" { t.Errorf("unexpected result (-want +got):\n%s", d) } } } func TestVulnerabilityStore_GetVulnerabilities_Aliases(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) vuln1 := &VulnerabilityHandle{ Name: "CVE-1234-5678", BlobValue: &VulnerabilityBlob{ ID: "CVE-1234-5678", Aliases: []string{"ALIAS-1", "ALIAS-2"}, }, Provider: &Provider{ ID: "provider!", }, } vuln2 := &VulnerabilityHandle{ Name: "ALIAS-1", BlobValue: &VulnerabilityBlob{ ID: "ALIAS-1", }, Provider: &Provider{ ID: "provider2!", }, } err := s.AddVulnerabilities(vuln1, vuln2) require.NoError(t, err) t.Run("include aliases", func(t *testing.T) { specifierWithAliases := &VulnerabilitySpecifier{ Name: "ALIAS-1", IncludeAliases: true, } results, err := s.GetVulnerabilities(specifierWithAliases, nil) require.NoError(t, err) require.Len(t, results, 2) assert.ElementsMatch(t, []string{"CVE-1234-5678", "ALIAS-1"}, []string{results[0].Name, results[1].Name}) }) t.Run("dont include aliases", func(t *testing.T) { specifierWithoutAliases := &VulnerabilitySpecifier{ Name: "ALIAS-1", IncludeAliases: false, } results, err := s.GetVulnerabilities(specifierWithoutAliases, nil) require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, "ALIAS-1", results[0].Name) }) t.Run("direct match without aliases", func(t *testing.T) { specifierDirectMatch := &VulnerabilitySpecifier{ Name: "CVE-1234-5678", IncludeAliases: false, } results, err := s.GetVulnerabilities(specifierDirectMatch, nil) require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, "CVE-1234-5678", results[0].Name) }) } func testVulnerabilityHandle() VulnerabilityHandle { now := time.Now() return VulnerabilityHandle{ Name: "CVE-8765-4321", Status: "status!", PublishedDate: &now, ModifiedDate: &now, WithdrawnDate: &now, Provider: &Provider{ ID: "provider!", }, BlobValue: &VulnerabilityBlob{ ID: "CVE-8765-4321", Assigners: []string{"assigner!"}, Description: "description!", References: []Reference{ { URL: "url!", Tags: []string{"tag!"}, }, }, Aliases: []string{"alias!"}, Severities: []Severity{ { Scheme: "scheme!", Value: "value!", Source: "source!", Rank: 10, }, { Scheme: SeveritySchemeCVSS, Value: CVSSSeverity{ Vector: "CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:P/VC:L/VI:H/VA:N/SC:N/SI:L/SA:N", Version: "4.0", }, }, }, }, } } func TestVulnerabilityStore_GetVulnerabilities_ByProviders(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) provider1 := &Provider{ID: "provider1"} provider2 := &Provider{ID: "provider2"} vuln1 := VulnerabilityHandle{Name: "CVE-1234-5678", BlobID: 1, Provider: provider1} vuln2 := VulnerabilityHandle{Name: "CVE-2345-6789", BlobID: 2, Provider: provider2} err := s.AddVulnerabilities(&vuln1, &vuln2) require.NoError(t, err) results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1"}}, nil) require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, vuln1.Name, results[0].Name) assert.Equal(t, vuln1.Provider.ID, results[0].ProviderID) results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1", "provider2"}}, nil) require.NoError(t, err) require.Len(t, results, 2) assert.ElementsMatch(t, []string{vuln1.Name, vuln2.Name}, []string{results[0].Name, results[1].Name}) } func TestVulnerabilityStore_GetVulnerabilities_FilterByMultipleFactors(t *testing.T) { db := setupTestStore(t).db bw := newBlobStore(db) s := newVulnerabilityStore(db, bw) now := time.Now() oneDayAgo := now.Add(-24 * time.Hour) halfDayAgo := now.Add(-12 * time.Hour) tenDaysAgo := now.Add(-240 * time.Hour) provider1 := &Provider{ID: "provider1"} provider2 := &Provider{ID: "provider2"} vuln1 := VulnerabilityHandle{ Name: "CVE-1234-5678", BlobID: 1, Provider: provider1, PublishedDate: &halfDayAgo, } vuln2 := VulnerabilityHandle{ Name: "CVE-2345-6789", BlobID: 2, Provider: provider2, // filtered out due to provider PublishedDate: &now, } vuln3 := VulnerabilityHandle{ Name: "CVE-1234-5678", BlobID: 3, Provider: provider1, PublishedDate: &tenDaysAgo, // filtered out due to date } err := s.AddVulnerabilities(&vuln1, &vuln2, &vuln3) require.NoError(t, err) results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{ Providers: []string{"provider1"}, // filter by provider... PublishedAfter: &oneDayAgo, // filter by date published... }, nil) require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, vuln1.Name, results[0].Name) } ================================================ FILE: grype/db/v6/vulnerability_test.go ================================================ package v6 import ( "strings" "testing" "time" "unicode" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func TestV5Namespace(t *testing.T) { // provider input should be derived from the Providers table: // +------------+---------+---------------+----------------------------------+------------------------+ // | id | version | processor | date_captured | input_digest | // +------------+---------+---------------+----------------------------------+------------------------+ // | nvd | 2 | vunnel@0.29.0 | 2025-01-08 01:32:55.179881+00:00 | xxh64:0a160d2b53dd0208 | // | alpine | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.824872+00:00 | xxh64:30c5b7b8efa0c087 | // | amazon | 1 | vunnel@0.29.0 | 2025-01-08 01:31:28.837469+00:00 | xxh64:7d90b3fa66b183bc | // | chainguard | 1 | vunnel@0.29.0 | 2025-01-08 01:31:26.969865+00:00 | xxh64:25a82fa97ac9e077 | // | debian | 1 | vunnel@0.29.0 | 2025-01-08 01:31:50.718966+00:00 | xxh64:4b1834b9e4e68987 | // | github | 1 | vunnel@0.29.0 | 2025-01-08 01:31:27.450124+00:00 | xxh64:a3ee6b48d37a0124 | // | mariner | 1 | vunnel@0.29.0 | 2025-01-08 01:32:35.005761+00:00 | xxh64:cb4f5861a1fda0af | // | oracle | 1 | vunnel@0.29.0 | 2025-01-08 01:32:33.696274+00:00 | xxh64:72c0a15731e96ab3 | // | rhel | 1 | vunnel@0.29.0 | 2025-01-08 01:32:32.192345+00:00 | xxh64:abf5d2fd5a26c194 | // | sles | 1 | vunnel@0.29.0 | 2025-01-08 01:32:42.988937+00:00 | xxh64:8f558f8f28a04489 | // | ubuntu | 3 | vunnel@0.29.0 | 2025-01-08 01:33:25.795537+00:00 | xxh64:97ef8421c0093620 | // | wolfi | 1 | vunnel@0.29.0 | 2025-01-08 01:32:58.571417+00:00 | xxh64:f294f3474d35b1a9 | // +------------+---------+---------------+----------------------------------+------------------------+ // the expected results should mimic what is found as v5 namespace values: // +--------------------------------------+ // | namespace | // +--------------------------------------+ // | nvd:cpe | // | github:language:javascript | // | ubuntu:distro:ubuntu:14.04 | // | ubuntu:distro:ubuntu:16.04 | // | ubuntu:distro:ubuntu:18.04 | // | ubuntu:distro:ubuntu:20.04 | // | ubuntu:distro:ubuntu:22.04 | // | ubuntu:distro:ubuntu:22.10 | // | ubuntu:distro:ubuntu:23.04 | // | ubuntu:distro:ubuntu:23.10 | // | ubuntu:distro:ubuntu:24.10 | // | debian:distro:debian:8 | // | debian:distro:debian:9 | // | ubuntu:distro:ubuntu:12.04 | // | ubuntu:distro:ubuntu:15.04 | // | sles:distro:sles:15 | // | sles:distro:sles:15.1 | // | sles:distro:sles:15.2 | // | sles:distro:sles:15.3 | // | sles:distro:sles:15.4 | // | sles:distro:sles:15.5 | // | sles:distro:sles:15.6 | // | amazon:distro:amazonlinux:2 | // | debian:distro:debian:10 | // | debian:distro:debian:11 | // | debian:distro:debian:12 | // | debian:distro:debian:unstable | // | oracle:distro:oraclelinux:6 | // | oracle:distro:oraclelinux:7 | // | oracle:distro:oraclelinux:8 | // | oracle:distro:oraclelinux:9 | // | redhat:distro:redhat:6 | // | redhat:distro:redhat:7 | // | redhat:distro:redhat:8 | // | redhat:distro:redhat:9 | // | ubuntu:distro:ubuntu:12.10 | // | ubuntu:distro:ubuntu:13.04 | // | ubuntu:distro:ubuntu:14.10 | // | ubuntu:distro:ubuntu:15.10 | // | ubuntu:distro:ubuntu:16.10 | // | ubuntu:distro:ubuntu:17.04 | // | ubuntu:distro:ubuntu:17.10 | // | ubuntu:distro:ubuntu:18.10 | // | ubuntu:distro:ubuntu:19.04 | // | ubuntu:distro:ubuntu:19.10 | // | ubuntu:distro:ubuntu:20.10 | // | ubuntu:distro:ubuntu:21.04 | // | ubuntu:distro:ubuntu:21.10 | // | ubuntu:distro:ubuntu:24.04 | // | github:language:php | // | debian:distro:debian:13 | // | debian:distro:debian:7 | // | redhat:distro:redhat:5 | // | sles:distro:sles:11.1 | // | sles:distro:sles:11.3 | // | sles:distro:sles:11.4 | // | sles:distro:sles:11.2 | // | sles:distro:sles:12 | // | sles:distro:sles:12.1 | // | sles:distro:sles:12.2 | // | sles:distro:sles:12.3 | // | sles:distro:sles:12.4 | // | sles:distro:sles:12.5 | // | chainguard:distro:chainguard:rolling | // | wolfi:distro:wolfi:rolling | // | minimos:distro:minimos:rolling | // | github:language:go | // | alpine:distro:alpine:3.20 | // | alpine:distro:alpine:3.21 | // | alpine:distro:alpine:edge | // | github:language:rust | // | github:language:python | // | sles:distro:sles:11 | // | oracle:distro:oraclelinux:5 | // | github:language:ruby | // | github:language:dotnet | // | alpine:distro:alpine:3.12 | // | alpine:distro:alpine:3.13 | // | alpine:distro:alpine:3.14 | // | alpine:distro:alpine:3.15 | // | alpine:distro:alpine:3.16 | // | alpine:distro:alpine:3.17 | // | alpine:distro:alpine:3.18 | // | alpine:distro:alpine:3.19 | // | mariner:distro:mariner:2.0 | // | github:language:java | // | github:language:dart | // | amazon:distro:amazonlinux:2023 | // | alpine:distro:alpine:3.10 | // | alpine:distro:alpine:3.11 | // | alpine:distro:alpine:3.4 | // | alpine:distro:alpine:3.5 | // | alpine:distro:alpine:3.7 | // | alpine:distro:alpine:3.8 | // | alpine:distro:alpine:3.9 | // | mariner:distro:azurelinux:3.0 | // | mariner:distro:mariner:1.0 | // | alpine:distro:alpine:3.3 | // | alpine:distro:alpine:3.6 | // | amazon:distro:amazonlinux:2022 | // | alpine:distro:alpine:3.2 | // | github:language:swift | // +--------------------------------------+ type testCase struct { name string provider string // from Providers.id ecosystem string // only used when provider non-os provider packageName string // only used for msrc osName string // only used for OS-based providers osVersion string // only used for OS-based providers expected string // } tests := []testCase{ // NVD { name: "nvd provider", provider: "nvd", expected: "nvd:cpe", }, // GitHub ecosystem tests { name: "github golang direct", provider: "github", ecosystem: "golang", expected: "github:language:go", }, { name: "github go-module ecosystem", provider: "github", ecosystem: "go-module", expected: "github:language:go", }, { name: "github composer ecosystem", provider: "github", ecosystem: "composer", expected: "github:language:php", }, { name: "github php-composer ecosystem", provider: "github", ecosystem: "php-composer", expected: "github:language:php", }, { name: "github cargo ecosystem", provider: "github", ecosystem: "cargo", expected: "github:language:rust", }, { name: "github rust-crate ecosystem", provider: "github", ecosystem: "rust-crate", expected: "github:language:rust", }, { name: "github pub ecosystem", provider: "github", ecosystem: "pub", expected: "github:language:dart", }, { name: "github dart-pub ecosystem", provider: "github", ecosystem: "dart-pub", expected: "github:language:dart", }, { name: "github nuget ecosystem", provider: "github", ecosystem: "nuget", expected: "github:language:dotnet", }, { name: "github maven ecosystem", provider: "github", ecosystem: "maven", expected: "github:language:java", }, { name: "github java ecosystem", provider: "github", ecosystem: "java", expected: "github:language:java", }, { name: "syft pkg type java-archive", provider: "github", ecosystem: "java-archive", expected: "github:language:java", }, { name: "github swifturl ecosystem", provider: "github", ecosystem: "swifturl", expected: "github:language:swift", }, { name: "github npm ecosystem", provider: "github", ecosystem: "npm", expected: "github:language:javascript", }, { name: "github node ecosystem", provider: "github", ecosystem: "node", expected: "github:language:javascript", }, { name: "github pypi ecosystem", provider: "github", ecosystem: "pypi", expected: "github:language:python", }, { name: "github pip ecosystem", provider: "github", ecosystem: "pip", expected: "github:language:python", }, { name: "github rubygems ecosystem", provider: "github", ecosystem: "rubygems", expected: "github:language:ruby", }, { name: "github gem ecosystem", provider: "github", ecosystem: "gem", expected: "github:language:ruby", }, // OS Distribution tests { name: "ubuntu distribution", provider: "ubuntu", osName: "ubuntu", osVersion: "22.04", expected: "ubuntu:distro:ubuntu:22.04", }, { name: "ubuntu distribution (trimmed 0s)", provider: "ubuntu", osName: "ubuntu", osVersion: "22.4", expected: "ubuntu:distro:ubuntu:22.04", }, { name: "redhat distribution", provider: "rhel", osName: "redhat", osVersion: "8", expected: "redhat:distro:redhat:8", }, { name: "debian distribution", provider: "debian", osName: "debian", osVersion: "11", expected: "debian:distro:debian:11", }, { name: "sles distribution", provider: "sles", osName: "sles", osVersion: "15.5", expected: "sles:distro:sles:15.5", }, { name: "alpine distribution", provider: "alpine", osName: "alpine", osVersion: "3.18", expected: "alpine:distro:alpine:3.18", }, { name: "chainguard distribution", provider: "chainguard", osName: "chainguard", osVersion: "rolling", expected: "chainguard:distro:chainguard:rolling", }, { name: "wolfi distribution", provider: "wolfi", osName: "wolfi", osVersion: "rolling", expected: "wolfi:distro:wolfi:rolling", }, { name: "minimos distribution", provider: "minimos", osName: "minimos", osVersion: "rolling", expected: "minimos:distro:minimos:rolling", }, { name: "amazon linux distribution", provider: "amazon", osName: "amazon", osVersion: "2023", expected: "amazon:distro:amazonlinux:2023", }, { name: "mariner regular version", provider: "mariner", osName: "mariner", osVersion: "2.0", expected: "mariner:distro:mariner:2.0", }, { name: "mariner regular version (not exact match)", provider: "mariner", osName: "mariner", osVersion: "2.1", expected: "mariner:distro:mariner:2.1", }, { name: "mariner regular version (auto fill minor version)", provider: "mariner", osName: "mariner", osVersion: "1", expected: "mariner:distro:mariner:1.0", }, { name: "mariner azure version", provider: "mariner", osName: "mariner", osVersion: "3.0", expected: "mariner:distro:azurelinux:3.0", }, { name: "mariner azure version (missing version fields)", provider: "mariner", osName: "mariner", osVersion: "3", expected: "mariner:distro:azurelinux:3.0", }, { name: "azurelinux version (extra version fields)", provider: "mariner", osName: "azurelinux", osVersion: "3.0.20240727", expected: "mariner:distro:azurelinux:3.0", }, { name: "azurelinux version", provider: "mariner", osName: "azurelinux", osVersion: "3.0", expected: "mariner:distro:azurelinux:3.0", }, { name: "azurelinux version (missing version fields)", provider: "mariner", osName: "azurelinux", osVersion: "3", expected: "mariner:distro:azurelinux:3.0", }, { name: "mariner azure version (extra version fields)", provider: "mariner", osName: "mariner", osVersion: "3.0.20240727", expected: "mariner:distro:azurelinux:3.0", }, { name: "oracle linux distribution", provider: "oracle", osName: "oracle", osVersion: "8", expected: "oracle:distro:oraclelinux:8", }, { name: "echo distribution", provider: "echo", osName: "echo", osVersion: "rolling", expected: "echo:distro:echo:rolling", }, { name: "minimos distribution", provider: "minimos", osName: "minimos", osVersion: "rolling", expected: "minimos:distro:minimos:rolling", }, // Version truncation tests { name: "rhel with minor version", provider: "rhel", osName: "redhat", osVersion: "8.6", expected: "redhat:distro:redhat:8", }, { name: "rhel with patch version", provider: "rhel", osName: "redhat", osVersion: "9.2.1", expected: "redhat:distro:redhat:9", }, { name: "oracle with minor version", provider: "oracle", osName: "oracle", osVersion: "8.7", expected: "oracle:distro:oraclelinux:8", }, { name: "oracle with patch version", provider: "oracle", osName: "oracle", osVersion: "9.3.1", expected: "oracle:distro:oraclelinux:9", }, // msrc is modeled as a distro for v5 but is just a package in v6 { name: "microsoft msrc-kb", provider: "msrc", ecosystem: "msrc-kb", packageName: "10012", expected: "msrc:distro:windows:10012", }, // new provider existing ecosystem { name: "grizzly go-module", provider: "grizzly", ecosystem: "go-module", expected: "grizzly:language:go", }, // new provider new ecosystem { name: "armadillo pizza", provider: "armadillo", ecosystem: "pizza", expected: "armadillo:language:pizza", }, // new OS { name: "gothmog", provider: "gothmog", osName: "gothmoglinux", osVersion: "zzzzzz11123", expected: "gothmog:distro:gothmoglinux:zzzzzz11123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { vuln := &VulnerabilityHandle{ Provider: &Provider{ ID: tt.provider, }, } pkg := &AffectedPackageHandle{} if tt.osName != "" { major, minor, _ := majorMinorPatch(tt.osVersion) var label string if major == "" { label = tt.osVersion } pkg.OperatingSystem = &OperatingSystem{ Name: tt.osName, MajorVersion: major, MinorVersion: minor, LabelVersion: label, } pkg.Package = &Package{ Name: "os-package", Ecosystem: "os-ecosystem", } } else if tt.ecosystem != "" { pkg.Package = &Package{ Ecosystem: tt.ecosystem, Name: tt.packageName, } } result := MimicV5Namespace(vuln, pkg) assert.Equal(t, tt.expected, result) }) } } func Test_getRelatedVulnerabilities(t *testing.T) { tests := []struct { name string vuln VulnerabilityHandle affected PackageBlob language string expected []vulnerability.Reference }{ { name: "GHSA with related CVEs", vuln: VulnerabilityHandle{ Name: "GHSA-1234", ProviderID: githubProvider, BlobValue: &VulnerabilityBlob{ Aliases: []string{"CVE-2024-1"}, }, }, affected: PackageBlob{ CVEs: []string{"CVE-2024-2", "CVE-2024-3"}, }, language: "python", expected: []vulnerability.Reference{ {ID: "CVE-2024-1", Namespace: v5NvdNamespace}, {ID: "CVE-2024-2", Namespace: v5NvdNamespace}, {ID: "CVE-2024-3", Namespace: v5NvdNamespace}, }, }, { name: "CGA with related GHSA", vuln: VulnerabilityHandle{ Name: "CGA-1234", ProviderID: githubProvider, BlobValue: &VulnerabilityBlob{ Aliases: []string{"GHSA-1234"}, }, }, affected: PackageBlob{ CVEs: []string{"CVE-2024-2", "CVE-2024-3"}, }, language: "python", expected: []vulnerability.Reference{ {ID: "GHSA-1234", Namespace: "github:language:python"}, {ID: "CVE-2024-2", Namespace: v5NvdNamespace}, {ID: "CVE-2024-3", Namespace: v5NvdNamespace}, }, }, { name: "CVE with related CVEs", vuln: VulnerabilityHandle{ Name: "CVE-2024-1234", ProviderID: "rhel", BlobValue: &VulnerabilityBlob{ Aliases: []string{"CVE-2024-1"}, }, }, affected: PackageBlob{ CVEs: []string{"CVE-2024-2", "CVE-2024-3"}, }, expected: []vulnerability.Reference{ {ID: "CVE-2024-1234", Namespace: v5NvdNamespace}, {ID: "CVE-2024-1", Namespace: v5NvdNamespace}, {ID: "CVE-2024-2", Namespace: v5NvdNamespace}, {ID: "CVE-2024-3", Namespace: v5NvdNamespace}, }, }, { name: "nvd CVE skips related CVEs to self", vuln: VulnerabilityHandle{ Name: "CVE-2024-1234", ProviderID: nvdProvider, BlobValue: &VulnerabilityBlob{ Aliases: []string{"CVE-2024-1", "CVE-2024-1234"}, }, }, affected: PackageBlob{ CVEs: []string{"CVE-2024-2", "CVE-2024-1234"}, }, expected: []vulnerability.Reference{ {ID: "CVE-2024-1", Namespace: v5NvdNamespace}, {ID: "CVE-2024-2", Namespace: v5NvdNamespace}, }, // does not include "CVE-2024-1234" }, { name: "non-nvd CVE with related nvd CVEs to self", vuln: VulnerabilityHandle{ Name: "CVE-2024-1234", ProviderID: "rhel", BlobValue: &VulnerabilityBlob{ Aliases: []string{"CVE-2024-1", "CVE-2024-1234"}, }, }, affected: PackageBlob{ CVEs: []string{"CVE-2024-2", "CVE-2024-1234"}, }, expected: []vulnerability.Reference{ {ID: "CVE-2024-1", Namespace: v5NvdNamespace}, {ID: "CVE-2024-2", Namespace: v5NvdNamespace}, {ID: "CVE-2024-1234", Namespace: v5NvdNamespace}, }, // does include "CVE-2024-1234" }, { name: "non-nvd CVE always relates back to NVD", vuln: VulnerabilityHandle{ Name: "CVE-2024-1234", ProviderID: "rhel", BlobValue: &VulnerabilityBlob{ Aliases: []string{}, }, }, affected: PackageBlob{ CVEs: []string{}, }, expected: []vulnerability.Reference{ {ID: "CVE-2024-1234", Namespace: v5NvdNamespace}, }, // does include "CVE-2024-1234" }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := getRelatedVulnerabilities(&tt.vuln, &tt.affected, tt.language) require.ElementsMatch(t, tt.expected, got) }) } } func TestToFix(t *testing.T) { fixedDate := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) anotherDate := time.Date(2024, 2, 1, 12, 0, 0, 0, time.UTC) tests := []struct { name string affectedRanges []Range expectedFix vulnerability.Fix }{ { name: "empty affected ranges", affectedRanges: []Range{}, expectedFix: vulnerability.Fix{}, }, { name: "all ranges have nil fix", affectedRanges: []Range{ {Fix: nil}, {Fix: nil}, }, expectedFix: vulnerability.Fix{}, }, { name: "single fixed version without available info", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3"}, State: vulnerability.FixStateFixed, Available: nil, }, }, { name: "single fixed version with complete available info", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: &fixedDate, Kind: "first-observed", }, }, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3"}, State: vulnerability.FixStateFixed, Available: []vulnerability.FixAvailable{ { Version: "1.2.3", Date: fixedDate, Kind: "first-observed", }, }, }, }, { name: "fix detail is nil - no available info", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, Detail: nil, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3"}, State: vulnerability.FixStateFixed, Available: nil, }, }, { name: "fix detail available is nil - no available info", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, Detail: &FixDetail{ Available: nil, }, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3"}, State: vulnerability.FixStateFixed, Available: nil, }, }, { name: "fix detail available date is nil - no available info", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: nil, Kind: "first-observed", }, }, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3"}, State: vulnerability.FixStateFixed, Available: nil, }, }, { name: "multiple fixed versions with mixed available info", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: &fixedDate, Kind: "first-observed", }, }, }, }, { Fix: &Fix{ Version: "1.3.0", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: nil, // this should not create an available entry Kind: "first-observed", }, }, }, }, { Fix: &Fix{ Version: "1.4.0", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: &anotherDate, Kind: "first-observed", }, }, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3", "1.3.0", "1.4.0"}, State: vulnerability.FixStateFixed, Available: []vulnerability.FixAvailable{ { Version: "1.2.3", Date: fixedDate, Kind: "first-observed", }, { Version: "1.4.0", Date: anotherDate, Kind: "first-observed", }, }, }, }, { name: "wont fix status only", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: WontFixStatus, }, }, }, expectedFix: vulnerability.Fix{ Versions: nil, State: vulnerability.FixStateWontFix, Available: nil, }, }, { name: "not fixed status only", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: NotFixedStatus, }, }, }, expectedFix: vulnerability.Fix{ Versions: nil, State: vulnerability.FixStateNotFixed, Available: nil, }, }, { name: "not affected status - not handled yet", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: NotAffectedFixStatus, }, }, }, expectedFix: vulnerability.Fix{}, }, { name: "fixed status overrides wont fix", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: WontFixStatus, }, }, { Fix: &Fix{ Version: "1.3.0", State: FixedStatus, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.3.0"}, State: vulnerability.FixStateFixed, Available: nil, }, }, { name: "fixed status overrides not fixed", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: NotFixedStatus, }, }, { Fix: &Fix{ Version: "1.3.0", State: FixedStatus, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.3.0"}, State: vulnerability.FixStateFixed, Available: nil, }, }, { name: "wont fix overrides not fixed", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: NotFixedStatus, }, }, { Fix: &Fix{ Version: "1.3.0", State: WontFixStatus, }, }, }, expectedFix: vulnerability.Fix{ Versions: nil, State: vulnerability.FixStateWontFix, Available: nil, }, }, { name: "mix of nil fixes and various states", affectedRanges: []Range{ {Fix: nil}, { Fix: &Fix{ Version: "1.2.3", State: WontFixStatus, }, }, {Fix: nil}, { Fix: &Fix{ Version: "1.3.0", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: &fixedDate, Kind: "first-observed", }, }, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.3.0"}, State: vulnerability.FixStateFixed, Available: []vulnerability.FixAvailable{ { Version: "1.3.0", Date: fixedDate, Kind: "first-observed", }, }, }, }, { name: "available with empty kind field", affectedRanges: []Range{ { Fix: &Fix{ Version: "1.2.3", State: FixedStatus, Detail: &FixDetail{ Available: &FixAvailability{ Date: &fixedDate, Kind: "", // empty kind should still work }, }, }, }, }, expectedFix: vulnerability.Fix{ Versions: []string{"1.2.3"}, State: vulnerability.FixStateFixed, Available: []vulnerability.FixAvailable{ { Version: "1.2.3", Date: fixedDate, Kind: "", }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := toFix(tt.affectedRanges) require.Equal(t, tt.expectedFix, got) }) } } func majorMinorPatch(ver string) (string, string, string) { if !unicode.IsDigit(rune(ver[0])) { return "", "", "" } parts := strings.Split(ver, ".") if len(parts) == 0 { return "", "", "" } if len(parts) == 1 { return parts[0], "", "" } if len(parts) == 2 { return parts[0], parts[1], "" } return parts[0], parts[1], parts[2] } ================================================ FILE: grype/deprecated.go ================================================ package grype import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/source" ) // TODO: deprecated, will remove before v1.0.0 func FindVulnerabilities(store vulnerability.Provider, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) { providerConfig := pkg.ProviderConfig{ SyftProviderConfig: pkg.SyftProviderConfig{ RegistryOptions: registryOptions, SBOMOptions: syft.DefaultCreateSBOMConfig(), }, } providerConfig.SBOMOptions.Search.Scope = scopeOpt packages, context, _, err := pkg.Provide(userImageStr, providerConfig) if err != nil { return match.Matches{}, pkg.Context{}, nil, err } matchers := matcher.NewDefaultMatchers(matcher.Config{}) return FindVulnerabilitiesForPackage(store, matchers, packages), context, packages, nil } // TODO: deprecated, will remove before v1.0.0 func FindVulnerabilitiesForPackage(store vulnerability.Provider, matchers []match.Matcher, packages []pkg.Package) match.Matches { exclusionProvider, _ := store.(match.ExclusionProvider) // TODO v5 is an exclusion provider, but v6 is not runner := VulnerabilityMatcher{ VulnerabilityProvider: store, ExclusionProvider: exclusionProvider, Matchers: matchers, NormalizeByCVE: false, } actualResults, _, err := runner.FindMatches(packages, pkg.Context{}) if err != nil || actualResults == nil { log.WithFields("error", err).Error("unable to find vulnerabilities") return match.NewMatches() } return *actualResults } ================================================ FILE: grype/distro/distro.go ================================================ package distro import ( "fmt" "strings" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/linux" ) // Distro represents a Linux Distribution. type Distro struct { Type Type Version string // major.minor.patch Codename string // in lieu of a version e.g. "fossa" instead of "20.04" Channels []string // distinguish between different feeds for fix and vulnerability data, e.g. "eus" for RHEL IDLike []string // fields populated in the constructor major string minor string remaining string } // New creates a new Distro object populated with the given values. func New(t Type, version, label string, idLikes ...string) *Distro { major, minor, remaining, versionWithoutSuffix, channels := parseVersion(version) for i := range idLikes { typ, ok := IDMapping[strings.TrimSpace(idLikes[i])] if ok { idLikes[i] = typ.String() } } return &Distro{ Type: t, Version: versionWithoutSuffix, Codename: label, IDLike: idLikes, Channels: channels, major: major, minor: minor, remaining: remaining, } } func parseVersion(version string) (major, minor, remaining, versionWithoutSuffix string, channels []string) { if version == "" { return "", "", "", "", nil } versionWithoutSuffix = version var channelStr string if strings.Contains(version, "+") { vParts := strings.SplitN(version, "+", 2) version = vParts[0] versionWithoutSuffix = version channelStr = vParts[1] } version = strings.TrimPrefix(version, "v") // if starts with a digit, then assume it's a version and extract the major, minor, and remaining versions if version[0] >= '0' && version[0] <= '9' { // extract the major, minor, and remaining versions parts := strings.Split(version, ".") if len(parts) > 0 { major = parts[0] if len(parts) > 1 { minor = parts[1] } if len(parts) > 2 { remaining = strings.Join(parts[2:], ".") } } } return major, minor, remaining, versionWithoutSuffix, stringutil.SplitOnAny(strings.TrimSpace(channelStr), ",", "+") } // ParseDistroString parses a user-provided distro string in the format "nameversion" // where separator can be "-", ":", or "@". It handles the special case of opensuse-leap // which contains a hyphen in its distro ID. Returns the distro name and version parts. func ParseDistroString(s string) (name, version string) { if s == "" { return "", "" } s = strings.TrimSpace(s) // Special handling for opensuse-leap which has a hyphen in its ID // Check if it starts with "opensuse-leap" and handle accordingly const opensuseLeap = "opensuse-leap" if strings.HasPrefix(strings.ToLower(s), opensuseLeap) { // Check if there's a separator after "opensuse-leap" remaining := s[len(opensuseLeap):] if len(remaining) == 0 { return opensuseLeap, "" } // If the next character is a separator, split there if remaining[0] == '-' || remaining[0] == ':' || remaining[0] == '@' { return opensuseLeap, strings.TrimSpace(remaining[1:]) } // Otherwise, treat the whole thing as the name return s, "" } // Find the first occurrence of any separator separators := []string{"-", ":", "@"} minIdx := len(s) foundSep := "" for _, sep := range separators { if idx := strings.Index(s, sep); idx != -1 && idx < minIdx { minIdx = idx foundSep = sep } } if foundSep == "" { return s, "" } return strings.TrimSpace(s[:minIdx]), strings.TrimSpace(s[minIdx+len(foundSep):]) } // NewFromNameVersion creates a new Distro object derived from the provided name and version func NewFromNameVersion(name, version string) *Distro { var codename string // if there are no digits in the version, it is likely a codename if !strings.ContainsAny(version, "0123456789") { codename = version version = "" } typ := IDMapping[name] if typ == "" { typ = Type(name) } return New(typ, version, codename, string(typ)) } // FromRelease attempts to get a distro from the linux release, only logging any errors func FromRelease(linuxRelease *linux.Release, channels []FixChannel) *Distro { if linuxRelease == nil { return nil } d, err := NewFromRelease(*linuxRelease, channels) if err != nil { log.WithFields("error", err).Warn("unable to create distro from linux distribution") } return d } // NewFromRelease creates a new Distro object derived from a syft linux.Release object. func NewFromRelease(release linux.Release, channels []FixChannel) (*Distro, error) { t := TypeFromRelease(release) if t == "" { return nil, fmt.Errorf("unable to determine distro type") } var ( selectedVersion string selectedVersionObj *version.Version ) for _, ver := range []string{release.VersionID, release.Version} { if ver == "" { continue } selectedVersionObj = version.New(ver, version.SemanticFormat) if selectedVersionObj.Validate() == nil { selectedVersion = ver break } } if selectedVersion == "" { selectedVersion = release.VersionID } d := New(t, selectedVersion, release.VersionCodename, release.IDLike...) d.Channels = applyChannels(release, selectedVersionObj, d.Channels, channels) return d, nil } func (d Distro) Name() string { return string(d.Type) } // MajorVersion returns the major version value from the pseudo-semantically versioned distro version value. func (d Distro) MajorVersion() string { return d.major } // MinorVersion returns the minor version value from the pseudo-semantically versioned distro version value. func (d Distro) MinorVersion() string { return d.minor } func (d Distro) RemainingVersion() string { return d.remaining } // String returns a human-friendly representation of the Linux distribution. func (d Distro) String() string { return fmt.Sprintf("%s %s", d.ID(), d.VersionString()) } func (d Distro) ID() string { return typeToIDMapping[d.Type] } func (d Distro) VersionString() string { versionStr := "" if d.Version != "" { versionStr = d.Version } else if d.Codename != "" { versionStr = d.Codename } channels := nonEmptyStrings(d.Channels...) if len(channels) > 0 { versionStr += "+" + strings.Join(channels, ",") } return versionStr } func (d Distro) LabelVersion() string { if d.Codename != "" { return d.Codename } if d.major == "" && d.minor == "" && d.remaining == "" { return d.Version } return "" } func nonEmptyStrings(ss ...string) (res []string) { for _, s := range ss { if s != "" { res = append(res, s) } } return res } ================================================ FILE: grype/distro/distro_test.go ================================================ package distro import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/directorysource" ) func testFixChannels() []FixChannel { return DefaultFixChannels() } func Test_NewDistroFromRelease(t *testing.T) { tests := []struct { name string release linux.Release channels []FixChannel expected *Distro minor string major string expectErr require.ErrorAssertionFunc }{ { name: "go case: derive version from version-id", release: linux.Release{ ID: "centos", VersionID: "8", Version: "7", IDLike: []string{"rhel"}, }, channels: testFixChannels(), expected: &Distro{ Type: CentOS, Version: "8", IDLike: []string{"redhat"}, }, major: "8", minor: "", }, { name: "fallback to release name when release id is missing", release: linux.Release{ Name: "windows", VersionID: "8", }, channels: testFixChannels(), expected: &Distro{ Type: Windows, Version: "8", }, major: "8", minor: "", }, { name: "fallback to version when version-id missing", release: linux.Release{ ID: "centos", Version: "8", }, channels: testFixChannels(), expected: &Distro{ Type: CentOS, Version: "8", }, major: "8", minor: "", }, { // this enables matching on multiple OS versions at once name: "missing version or label version is allowed", release: linux.Release{ ID: "centos", }, channels: testFixChannels(), expected: &Distro{ Type: CentOS, }, }, { name: "bogus distro type results in error", release: linux.Release{ ID: "bogosity", VersionID: "8", }, channels: testFixChannels(), expectErr: require.Error, }, { // syft -o json debian:testing | jq .distro name: "unstable debian", release: linux.Release{ ID: "debian", VersionID: "", Version: "", PrettyName: "Debian GNU/Linux trixie/sid", VersionCodename: "trixie", Name: "Debian GNU/Linux", }, channels: testFixChannels(), expected: &Distro{ Type: Debian, Codename: "trixie", }, major: "", minor: "", }, { name: "azure linux 3", release: linux.Release{ ID: "azurelinux", Version: "3.0.20240417", VersionID: "3.0", }, channels: testFixChannels(), expected: &Distro{ Type: Azure, Version: "3.0", }, major: "3", minor: "0", }, { name: "eus hint ignored when configured to never apply", release: linux.Release{ ID: "rhel", Version: "9.4", ExtendedSupport: true, }, channels: testFixChannels(), expected: &Distro{ Type: RedHat, Version: "9.4", Channels: names("eus"), }, major: "9", minor: "4", }, { name: "eus hinted at as attribute", release: linux.Release{ ID: "rhel", Version: "9.4", ExtendedSupport: true, }, channels: []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, }, }, expected: &Distro{ Type: RedHat, Version: "9.4", Channels: names("eus"), }, major: "9", minor: "4", }, { name: "eus embedded in the version", release: linux.Release{ ID: "rhel", Version: "9.4+eus", }, channels: []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, }, }, expected: &Distro{ Type: RedHat, Version: "9.4", Channels: names("eus"), }, major: "9", minor: "4", }, { name: "eus hinted at as attribute (always apply)", release: linux.Release{ ID: "rhel", Version: "9.4", ExtendedSupport: true, }, channels: []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelAlwaysEnabled, // important! }, }, expected: &Distro{ Type: RedHat, Version: "9.4", Channels: names("eus"), }, major: "9", minor: "4", }, { name: "eus embedded in the version (always apply)", release: linux.Release{ ID: "rhel", Version: "9.4+eus", }, channels: []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelAlwaysEnabled, // important! }, }, expected: &Distro{ Type: RedHat, Version: "9.4", Channels: names("eus"), }, major: "9", minor: "4", }, { name: "eus hinted at as attribute (never apply)", release: linux.Release{ ID: "rhel", Version: "9.4", ExtendedSupport: true, }, channels: []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelNeverEnabled, // important! }, }, expected: &Distro{ Type: RedHat, Version: "9.4", }, major: "9", minor: "4", }, { name: "eus embedded in the version (never apply)", release: linux.Release{ ID: "rhel", Version: "9.4+eus", }, channels: []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelNeverEnabled, // important! }, }, expected: &Distro{ Type: RedHat, Version: "9.4", }, major: "9", minor: "4", }, { name: "v versionID prefix postmarketos", release: linux.Release{ ID: "postmarketos", VersionID: "v24.06", }, expected: &Distro{ Type: PostmarketOS, Version: "v24.06", }, major: "24", minor: "06", }, { name: "edge as versionID prefix postmarketos", release: linux.Release{ ID: "postmarketos", VersionID: "edge", }, expected: &Distro{ Type: PostmarketOS, Version: "edge", }, major: "", minor: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expectErr == nil { tt.expectErr = require.NoError } distro, err := NewFromRelease(tt.release, tt.channels) tt.expectErr(t, err) if err != nil { return } if d := cmp.Diff(tt.expected, distro, cmpopts.IgnoreUnexported(Distro{})); d != "" { t.Errorf("unexpected result: %s", d) } assert.Equal(t, tt.major, distro.MajorVersion(), "unexpected major version") assert.Equal(t, tt.minor, distro.MinorVersion(), "unexpected minor version") }) } } func Test_NewDistroFromRelease_Coverage(t *testing.T) { observedDistros := stringutil.NewStringSet() definedDistros := stringutil.NewStringSet() for _, distroType := range All { definedDistros.Add(string(distroType)) } // Somewhat cheating with Windows. There is no support for detecting/parsing a Windows OS, so it is not // possible to comply with this test unless it is added manually to the "observed distros" definedDistros.Remove(string(Windows)) tests := []struct { Name string Type Type Version string LabelVersion string }{ { Name: "testdata/os/alpine", Type: Alpine, Version: "3.11.6", }, { Name: "testdata/os/alpine-edge", Type: Alpine, Version: "3.22.0_alpha20250108", }, { Name: "testdata/os/amazon", Type: AmazonLinux, Version: "2", }, { Name: "testdata/os/busybox", Type: Busybox, Version: "1.31.1", }, { Name: "testdata/os/centos", Type: CentOS, Version: "8", }, { Name: "testdata/os/debian", Type: Debian, Version: "8", }, { Name: "testdata/os/debian-sid", Type: Debian, LabelVersion: "trixie", }, { Name: "testdata/os/fedora", Type: Fedora, Version: "31", }, { Name: "testdata/os/redhat", Type: RedHat, Version: "7.3", }, { Name: "testdata/os/ubuntu", Type: Ubuntu, Version: "20.04", LabelVersion: "focal", }, { Name: "testdata/os/oraclelinux", Type: OracleLinux, Version: "8.3", }, { Name: "testdata/os/custom", Type: Scientific, Version: "8", }, { Name: "testdata/os/opensuse-leap", Type: OpenSuseLeap, Version: "15.2", }, { Name: "testdata/os/sles", Type: SLES, Version: "15.2", }, { Name: "testdata/os/photon", Type: Photon, Version: "2.0", }, { Name: "testdata/os/arch", Type: ArchLinux, }, { Name: "testdata/partial-fields/missing-id", Type: Debian, Version: "8", }, { Name: "testdata/partial-fields/unknown-id", Type: Debian, Version: "8", }, { Name: "testdata/os/centos6", Type: CentOS, Version: "6", }, { Name: "testdata/os/centos5", Type: CentOS, Version: "5.7", }, { Name: "testdata/os/mariner", Type: Mariner, Version: "1.0", }, { Name: "testdata/os/azurelinux", Type: Azure, Version: "3.0", }, { Name: "testdata/os/rockylinux", Type: RockyLinux, Version: "8.4", }, { Name: "testdata/os/almalinux", Type: AlmaLinux, Version: "8.4", }, { Name: "testdata/os/echo", Type: Echo, Version: "1", }, { Name: "testdata/os/gentoo", Type: Gentoo, }, { Name: "testdata/os/wolfi", Type: Wolfi, Version: "20220914", }, { Name: "testdata/os/chainguard", Type: Chainguard, Version: "20230214", }, { Name: "testdata/os/minimos", Type: MinimOS, Version: "20241031", }, { Name: "testdata/os/raspbian", Type: Raspbian, Version: "9", }, { Name: "testdata/os/scientific", Type: Scientific, Version: "7.5", }, { Name: "testdata/os/scientific6", Type: Scientific, Version: "6.10", }, { Name: "testdata/os/secureos", Type: SecureOS, Version: "2025.09.09", }, { Name: "testdata/os/postmarketos", Type: PostmarketOS, Version: "v25.06", }, { Name: "testdata/os/postmarketos-edge", Type: PostmarketOS, Version: "edge", LabelVersion: "edge", }, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { s, err := directorysource.NewFromPath(tt.Name) require.NoError(t, err) resolver, err := s.FileResolver(source.SquashedScope) require.NoError(t, err) // make certain syft and pick up on the raw information we need release := linux.IdentifyRelease(resolver) require.NotNil(t, release, "empty linux release info") // craft a new distro from the syft raw info d, err := NewFromRelease(*release, testFixChannels()) require.NoError(t, err) observedDistros.Add(d.Type.String()) assert.Equal(t, tt.Type, d.Type, "unexpected distro type") assert.Equal(t, tt.LabelVersion, d.LabelVersion(), "unexpected label version") assert.Equal(t, tt.Version, d.Version, "unexpected version") }) } // ensure that test cases stay in sync with the distros that can be identified if len(observedDistros) < len(definedDistros) { for _, d := range definedDistros.ToSlice() { t.Logf(" defined: %s", d) } for _, d := range observedDistros.ToSlice() { t.Logf(" observed: %s", d) } t.Errorf("distro coverage incomplete (defined=%d, coverage=%d)", len(definedDistros), len(observedDistros)) } } func TestDistro_FullVersion(t *testing.T) { tests := []struct { version string expected string }{ { version: "8", expected: "8", }, { version: "18.04", expected: "18.04", }, { version: "0", expected: "0", }, { version: "18.1.2", expected: "18.1.2", }, } for _, test := range tests { t.Run(test.version, func(t *testing.T) { d, err := NewFromRelease(linux.Release{ ID: "centos", Version: test.version, }, testFixChannels()) require.NoError(t, err) assert.Equal(t, test.expected, d.Version) }) } } func TestDistro_MajorVersion(t *testing.T) { tests := []struct { version string expected string }{ { version: "8", expected: "8", }, { version: "18.04", expected: "18", }, { version: "0", expected: "0", }, { version: "18.1.2", expected: "18", }, } for _, test := range tests { t.Run(test.version, func(t *testing.T) { d, err := NewFromRelease(linux.Release{ ID: "centos", Version: test.version, }, testFixChannels()) require.NoError(t, err) assert.Equal(t, test.expected, d.MajorVersion()) }) } } func names(ns ...string) []string { return ns } func TestParseDistroString(t *testing.T) { tests := []struct { name string input string expectedName string expectedVersion string }{ { name: "hyphen separator", input: "debian-11", expectedName: "debian", expectedVersion: "11", }, { name: "colon separator", input: "debian:11", expectedName: "debian", expectedVersion: "11", }, { name: "at separator", input: "debian@11", expectedName: "debian", expectedVersion: "11", }, { name: "no separator", input: "debian", expectedName: "debian", expectedVersion: "", }, { name: "with major.minor version", input: "ubuntu-20.04", expectedName: "ubuntu", expectedVersion: "20.04", }, { name: "with codename", input: "ubuntu@focal", expectedName: "ubuntu", expectedVersion: "focal", }, { name: "with channels", input: "rhel:9.4+eus", expectedName: "rhel", expectedVersion: "9.4+eus", }, { name: "opensuse-leap with hyphen separator", input: "opensuse-leap-15.2", expectedName: "opensuse-leap", expectedVersion: "15.2", }, { name: "opensuse-leap with colon separator", input: "opensuse-leap:15.2", expectedName: "opensuse-leap", expectedVersion: "15.2", }, { name: "opensuse-leap with at separator", input: "opensuse-leap@15.2", expectedName: "opensuse-leap", expectedVersion: "15.2", }, { name: "opensuse-leap without version", input: "opensuse-leap", expectedName: "opensuse-leap", expectedVersion: "", }, { name: "opensuse-leap with mixed case", input: "OpenSUSE-Leap-15.2", expectedName: "opensuse-leap", expectedVersion: "15.2", }, { name: "empty string", input: "", expectedName: "", expectedVersion: "", }, { name: "with whitespace", input: " debian : 11 ", expectedName: "debian", expectedVersion: "11", }, { name: "multiple separators uses first", input: "debian-11:test", expectedName: "debian", expectedVersion: "11:test", }, { name: "rhel with hyphen", input: "rhel-8", expectedName: "rhel", expectedVersion: "8", }, { name: "centos with colon", input: "centos:7", expectedName: "centos", expectedVersion: "7", }, { name: "alpine with at", input: "alpine@3.11", expectedName: "alpine", expectedVersion: "3.11", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { name, version := ParseDistroString(tt.input) assert.Equal(t, tt.expectedName, name, "unexpected name") assert.Equal(t, tt.expectedVersion, version, "unexpected version") }) } } ================================================ FILE: grype/distro/fix_channel.go ================================================ package distro import ( "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/linux" ) type FixChannelEnabled string const ( // ChannelNeverEnabled means that the channel should never be applied to the distro ChannelNeverEnabled FixChannelEnabled = "never" // ChannelAlwaysEnabled means that the channel should always be applied to the distro ChannelAlwaysEnabled FixChannelEnabled = "always" // ChannelConditionallyEnabled means that the channel should conditionally be applied to the distro if there is SBOM material that indicates the channel was configured at build time ChannelConditionallyEnabled FixChannelEnabled = "auto" ) // FixChannel represents a subscription or repository where package fixes and updates are provided for a Linux distribution type FixChannel struct { // Name is the name of the channel, e.g. "eus" for RHEL Name string // IDs is a list of distro release IDs that this channel applies to, e.g. "rhel" for RHEL (this is relative to the /etc/os-release ID field) IDs []string // Apply indicates how the channel should be applied to the distro Apply FixChannelEnabled // Versions is a version constraint that indicates which versions of the distro this channel applies to (e.g. ">= 8.0" for RHEL 8 and above) Versions version.Constraint } type FixChannels []FixChannel func DefaultFixChannels() FixChannels { return []FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat), }, } } func (f FixChannels) Apply(enable FixChannelEnabled) FixChannels { for i := range f { f[i].Apply = enable } return f } func (f FixChannels) Get(name string) *FixChannel { for i := range f { if strings.EqualFold(f[i].Name, name) { return &f[i] } } return nil } func applyChannels(release linux.Release, ver *version.Version, existingChannels []string, channels []FixChannel) []string { existingChannelSet := strset.New(existingChannels...) var result []string addResult := func(channel string, extendedSupport bool, pref FixChannelEnabled) { res := applyChannel(channel, extendedSupport, pref) if res != "" { result = append(result, res) } } for _, channel := range channels { var found bool for _, channelID := range channel.IDs { if release.ID == channelID { found = true break } } if !found { continue } // we will either get a direct indication as a flag, or as a result of the channel being applied to the distro already extendedSupport := release.ExtendedSupport || existingChannelSet.Has(channel.Name) if ver == nil && release.VersionCodename != "" { // TODO: there is not a good way to do this without a DB call, so for now we will assume the channel applies log.Debugf("using channel %q for distro %q with codename %q", channel.Name, release.ID, release.VersionCodename) addResult(channel.Name, extendedSupport, channel.Apply) continue } if channel.Versions != nil && ver != nil { isApplicable, err := channel.Versions.Satisfied(ver) if err != nil { log.WithFields("error", err, "constraint", channel.Versions).Debugf("unable to determine if channel %q is applicable for distro %q with version %q", channel.Name, release.ID, ver) continue } if isApplicable { log.Debugf("using channel %q for distro %q with version %q", channel.Name, release.ID, ver) addResult(channel.Name, extendedSupport, channel.Apply) continue } } log.Debugf("using channel %q for distro %q", channel.Name, release.ID) addResult(channel.Name, extendedSupport, channel.Apply) } return result } func applyChannel(channel string, hintsExtendedSupport bool, pref FixChannelEnabled) string { switch pref { case ChannelNeverEnabled: return "" case ChannelAlwaysEnabled: return channel case ChannelConditionallyEnabled: if hintsExtendedSupport { return channel } } return "" } ================================================ FILE: grype/distro/fix_channel_test.go ================================================ package distro import ( "testing" "github.com/google/go-cmp/cmp" "github.com/anchore/grype/grype/version" ) func TestDefaultFixChannels(t *testing.T) { channels := DefaultFixChannels() // this seems like a silly test, however, it is critical to ensure that the default channels have EUS with expected values expected := FixChannels{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat), }, } if diff := cmp.Diff(expected, channels); diff != "" { t.Errorf("DefaultFixChannels() mismatch (-want +got):\n%s", diff) } } func TestFixChannels_Apply(t *testing.T) { tests := []struct { name string channels FixChannels enable FixChannelEnabled want FixChannels }{ { name: "apply always enabled to single channel", channels: FixChannels{ { Name: "eus", Apply: ChannelConditionallyEnabled, }, }, enable: ChannelAlwaysEnabled, want: FixChannels{ { Name: "eus", Apply: ChannelAlwaysEnabled, }, }, }, { name: "apply never enabled to multiple channels", channels: FixChannels{ { Name: "eus", Apply: ChannelConditionallyEnabled, }, { Name: "main", Apply: ChannelAlwaysEnabled, }, }, enable: ChannelNeverEnabled, want: FixChannels{ { Name: "eus", Apply: ChannelNeverEnabled, }, { Name: "main", Apply: ChannelNeverEnabled, }, }, }, { name: "apply to empty channels", channels: FixChannels{}, enable: ChannelAlwaysEnabled, want: FixChannels{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.channels.Apply(tt.enable) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("FixChannels.Apply() mismatch (-want +got):\n%s", diff) } }) } } func TestFixChannels_Get(t *testing.T) { channels := FixChannels{ { Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, }, { Name: "main", IDs: []string{"debian", "ubuntu"}, Apply: ChannelAlwaysEnabled, }, } tests := []struct { name string channelName string want *FixChannel }{ { name: "find existing channel by exact name", channelName: "eus", want: &FixChannel{ Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, }, }, { name: "find existing channel by case insensitive name", channelName: "EUS", want: &FixChannel{ Name: "eus", IDs: []string{"rhel"}, Apply: ChannelConditionallyEnabled, }, }, { name: "find existing channel by mixed case name", channelName: "Main", want: &FixChannel{ Name: "main", IDs: []string{"debian", "ubuntu"}, Apply: ChannelAlwaysEnabled, }, }, { name: "channel not found", channelName: "nonexistent", want: nil, }, { name: "empty channel name", channelName: "", want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := channels.Get(tt.channelName) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("FixChannels.Get() mismatch (-want +got):\n%s", diff) } }) } } ================================================ FILE: grype/distro/testdata/bad-id ================================================ NAME="Red Hat Enterprise Linux" VERSION="8.1 (Ootpa)" ID_LIKE="fedora" PLATFORM_ID="platform:el8" PRETTY_NAME="Red Hat Enterprise Linux 8.1 (Ootpa)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:redhat:enterprise_linux:8.1:GA" HOME_URL="https://www.redhat.com/" BUG_REPORT_URL="https://bugzilla.redhat.com/" REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8" REDHAT_BUGZILLA_PRODUCT_VERSION=8.1 REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" REDHAT_SUPPORT_PRODUCT_VERSION="8.1" ================================================ FILE: grype/distro/testdata/bad-redhat-release ================================================ CentOS release 5 (Final) ================================================ FILE: grype/distro/testdata/bad-system-release-cpe ================================================ cpe:/o:centos:6:GA ================================================ FILE: grype/distro/testdata/centos-8 ================================================ NAME="CentOS Linux" VERSION="8 (Core)" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="8" PLATFORM_ID="platform:el8" PRETTY_NAME="CentOS Linux 8 (Core)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:centos:centos:8" HOME_URL="https://www.centos.org/" BUG_REPORT_URL="https://bugs.centos.org/" CENTOS_MANTISBT_PROJECT="CentOS-8" CENTOS_MANTISBT_PROJECT_VERSION="8" REDHAT_SUPPORT_PRODUCT="centos" REDHAT_SUPPORT_PRODUCT_VERSION="8" ================================================ FILE: grype/distro/testdata/debian-8 ================================================ PRETTY_NAME="Debian GNU/Linux 8 (jessie)" NAME="Debian GNU/Linux" VERSION_ID="8" VERSION="8 (jessie)" ID=debian HOME_URL="http://www.debian.org/" SUPPORT_URL="http://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ================================================ FILE: grype/distro/testdata/os/almalinux/etc/os-release ================================================ NAME="AlmaLinux" VERSION="8.4 (Electric Cheetah)" ID="almalinux" ID_LIKE="rhel centos fedora" VERSION_ID="8.4" PLATFORM_ID="platform:el8" PRETTY_NAME="AlmaLinux 8.4 (Electric Cheetah)" ANSI_COLOR="0;34" CPE_NAME="cpe:/o:almalinux:almalinux:8.4:GA" HOME_URL="https://almalinux.org/" DOCUMENTATION_URL="https://wiki.almalinux.org/" BUG_REPORT_URL="https://bugs.almalinux.org/" ALMALINUX_MANTISBT_PROJECT="AlmaLinux-8" ALMALINUX_MANTISBT_PROJECT_VERSION="8.4" ================================================ FILE: grype/distro/testdata/os/alpine/etc/os-release ================================================ NAME="Alpine Linux" ID=alpine VERSION_ID=3.11.6 PRETTY_NAME="Alpine Linux v3.11" HOME_URL="https://alpinelinux.org/" BUG_REPORT_URL="https://bugs.alpinelinux.org/" ================================================ FILE: grype/distro/testdata/os/alpine-edge/etc/os-release ================================================ NAME="Alpine Linux" ID=alpine VERSION_ID=3.22.0_alpha20250108 PRETTY_NAME="Alpine Linux edge" HOME_URL="https://alpinelinux.org/" BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues" ================================================ FILE: grype/distro/testdata/os/amazon/etc/os-release ================================================ NAME="Amazon Linux" VERSION="2" ID="amzn" ID_LIKE="centos rhel fedora" VERSION_ID="2" PRETTY_NAME="Amazon Linux 2" ANSI_COLOR="0;33" CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2" HOME_URL="https://amazonlinux.com/" ================================================ FILE: grype/distro/testdata/os/arch/etc/os-release ================================================ NAME="Arch Linux" PRETTY_NAME="Arch Linux" ID=arch BUILD_ID=rolling ANSI_COLOR="38;2;23;147;209" HOME_URL="https://www.archlinux.org/" DOCUMENTATION_URL="https://wiki.archlinux.org/" SUPPORT_URL="https://bbs.archlinux.org/" BUG_REPORT_URL="https://bugs.archlinux.org/" LOGO=archlinux ================================================ FILE: grype/distro/testdata/os/azurelinux/etc/os-release ================================================ NAME="Microsoft Azure Linux" VERSION="3.0.20240417" ID=azurelinux VERSION_ID="3.0" PRETTY_NAME="Microsoft Azure Linux 3.0" ANSI_COLOR="1;34" HOME_URL="https://aka.ms/azurelinux" BUG_REPORT_URL="https://aka.ms/azurelinux" SUPPORT_URL="https://aka.ms/azurelinux" ================================================ FILE: grype/distro/testdata/os/busybox/bin/busybox ================================================ junk...BusyBox v1.31.1more junk ================================================ FILE: grype/distro/testdata/os/centos/usr/lib/os-release ================================================ NAME="CentOS Linux" VERSION="8 (Core)" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="8" PLATFORM_ID="platform:el8" PRETTY_NAME="CentOS Linux 8 (Core)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:centos:centos:8" HOME_URL="https://www.centos.org/" BUG_REPORT_URL="https://bugs.centos.org/" CENTOS_MANTISBT_PROJECT="CentOS-8" CENTOS_MANTISBT_PROJECT_VERSION="8" REDHAT_SUPPORT_PRODUCT="centos" REDHAT_SUPPORT_PRODUCT_VERSION="8" ================================================ FILE: grype/distro/testdata/os/centos5/etc/redhat-release ================================================ CentOS release 5.7 (Final) ================================================ FILE: grype/distro/testdata/os/centos6/etc/system-release-cpe ================================================ cpe:/o:centos:linux:6:GA ================================================ FILE: grype/distro/testdata/os/chainguard/etc/os-release ================================================ ID=chainguard NAME="Chainguard" PRETTY_NAME="Chainguard" VERSION_ID="20230214" HOME_URL="https://chainguard.dev/" ================================================ FILE: grype/distro/testdata/os/custom/etc/os-release ================================================ NAME="Scientific Linux" VERSION="16 (Core)" ID="scientific" ID_LIKE="rhel fedora" VERSION_ID="8" PLATFORM_ID="platform:el8" PRETTY_NAME="CentOS Linux 8 (Core)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:centos:centos:8" HOME_URL="https://www.centos.org/" BUG_REPORT_URL="https://bugs.centos.org/" CENTOS_MANTISBT_PROJECT="CentOS-8" CENTOS_MANTISBT_PROJECT_VERSION="8" REDHAT_SUPPORT_PRODUCT="centos" REDHAT_SUPPORT_PRODUCT_VERSION="8" ================================================ FILE: grype/distro/testdata/os/debian/usr/lib/os-release ================================================ PRETTY_NAME="Debian GNU/Linux 8 (jessie)" NAME="Debian GNU/Linux" VERSION_ID="8" VERSION="8 (jessie)" ID=debian HOME_URL="http://www.debian.org/" SUPPORT_URL="http://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ================================================ FILE: grype/distro/testdata/os/debian-sid/usr/lib/os-release ================================================ PRETTY_NAME="Debian GNU/Linux trixie/sid" NAME="Debian GNU/Linux" VERSION_CODENAME=trixie ID=debian HOME_URL="https://www.debian.org/" SUPPORT_URL="https://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ================================================ FILE: grype/distro/testdata/os/echo/etc/os-release ================================================ NAME="Echo Linux" PRETTY_NAME="Echo Linux" ID="echo" ID_LIKE="debian" VERSION_ID="1" HOME_URL="https://echohq.com/" ================================================ FILE: grype/distro/testdata/os/empty/etc/os-release ================================================ ================================================ FILE: grype/distro/testdata/os/fedora/usr/lib/os-release ================================================ NAME=Fedora VERSION="31 (Container Image)" ID=fedora VERSION_ID=31 VERSION_CODENAME="" PLATFORM_ID="platform:f31" PRETTY_NAME="Fedora 31 (Container Image)" ANSI_COLOR="0;34" LOGO=fedora-logo-icon CPE_NAME="cpe:/o:fedoraproject:fedora:31" HOME_URL="https://fedoraproject.org/" DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/" SUPPORT_URL="https://fedoraproject.org/wiki/Communicating_and_getting_help" BUG_REPORT_URL="https://bugzilla.redhat.com/" REDHAT_BUGZILLA_PRODUCT="Fedora" REDHAT_BUGZILLA_PRODUCT_VERSION=31 REDHAT_SUPPORT_PRODUCT="Fedora" REDHAT_SUPPORT_PRODUCT_VERSION=31 PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy" VARIANT="Container Image" VARIANT_ID=container ================================================ FILE: grype/distro/testdata/os/gentoo/etc/os-release ================================================ NAME=Gentoo ID=gentoo PRETTY_NAME="Gentoo/Linux" ANSI_COLOR="1;32" HOME_URL="https://www.gentoo.org/" SUPPORT_URL="https://www.gentoo.org/support/" BUG_REPORT_URL="https://bugs.gentoo.org/" ================================================ FILE: grype/distro/testdata/os/mariner/etc/os-release ================================================ NAME="Common Base Linux Mariner" VERSION="1.0.20210901" ID=mariner VERSION_ID=1.0 PRETTY_NAME="CBL-Mariner/Linux" ANSI_COLOR="1;34" HOME_URL="https://aka.ms/cbl-mariner" BUG_REPORT_URL="https://aka.ms/cbl-mariner" SUPPORT_URL="https://aka.ms/cbl-mariner" ================================================ FILE: grype/distro/testdata/os/minimos/etc/os-release ================================================ ID=minimos NAME="MinimOS" PRETTY_NAME="MinimOS" VERSION_ID="20241031" HOME_URL="https://minimus.io" ================================================ FILE: grype/distro/testdata/os/opensuse-leap/etc/os-release ================================================ NAME="openSUSE Leap" VERSION="15.2" ID="opensuse-leap" ID_LIKE="suse opensuse" VERSION_ID="15.2" PRETTY_NAME="openSUSE Leap 15.2" ANSI_COLOR="0;32" CPE_NAME="cpe:/o:opensuse:leap:15.2" BUG_REPORT_URL="https://bugs.opensuse.org" HOME_URL="https://www.opensuse.org/" ================================================ FILE: grype/distro/testdata/os/oraclelinux/etc/os-release ================================================ NAME="Oracle Linux Server" VERSION="8.3" ID="ol" ID_LIKE="fedora" VARIANT="Server" VARIANT_ID="server" VERSION_ID="8.3" PLATFORM_ID="platform:el8" PRETTY_NAME="Oracle Linux Server 8.3" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:oracle:linux:8:3:server" HOME_URL="https://linux.oracle.com/" BUG_REPORT_URL="https://bugzilla.oracle.com/" ORACLE_BUGZILLA_PRODUCT="Oracle Linux 8" ORACLE_BUGZILLA_PRODUCT_VERSION=8.3 ORACLE_SUPPORT_PRODUCT="Oracle Linux" ORACLE_SUPPORT_PRODUCT_VERSION=8.3 ================================================ FILE: grype/distro/testdata/os/photon/etc/os-release ================================================ NAME="VMware Photon OS" VERSION="2.0" ID=photon VERSION_ID=2.0 PRETTY_NAME="VMware Photon OS/Linux" ANSI_COLOR="1;34" HOME_URL="https://vmware.github.io/photon/" BUG_REPORT_URL="https://github.com/vmware/photon/issues" ================================================ FILE: grype/distro/testdata/os/postmarketos/etc/os-release ================================================ PRETTY_NAME="postmarketOS v25.06" NAME="postmarketOS" VERSION_ID="v25.06" VERSION="v25.06" ID="postmarketos" ID_LIKE="alpine" HOME_URL="https://www.postmarketos.org/" SUPPORT_URL="https://gitlab.postmarketos.org/postmarketOS" BUG_REPORT_URL="https://gitlab.postmarketos.org/postmarketOS/pmaports/issues" LOGO="postmarketos-logo" ANSI_COLOR="0;32" ================================================ FILE: grype/distro/testdata/os/postmarketos-edge/etc/os-release ================================================ PRETTY_NAME="postmarketOS edge" NAME="postmarketOS" VERSION_ID="edge" VERSION="edge" ID="postmarketos" ID_LIKE="alpine" HOME_URL="https://www.postmarketos.org/" SUPPORT_URL="https://gitlab.postmarketos.org/postmarketOS" BUG_REPORT_URL="https://gitlab.postmarketos.org/postmarketOS/pmaports/issues" LOGO="postmarketos-logo" ANSI_COLOR="0;32" ================================================ FILE: grype/distro/testdata/os/raspbian/etc/os-release ================================================ PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)" NAME="Raspbian GNU/Linux" VERSION_ID="9" VERSION="9 (stretch)" ID=raspbian ID_LIKE=debian HOME_URL="http://www.raspbian.org/" SUPPORT_URL="http://www.raspbian.org/RaspbianForums" BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs" ================================================ FILE: grype/distro/testdata/os/redhat/usr/lib/os-release ================================================ NAME="Red Hat Enterprise Linux Server" VERSION="7.3 (Maipo)" ID="rhel" ID_LIKE="fedora" VERSION_ID="7.3" PRETTY_NAME="Red Hat Enterprise Linux Server 7.3 (Maipo)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:redhat:enterprise_linux:7.3:GA:server" HOME_URL="https://www.redhat.com/" BUG_REPORT_URL="https://bugzilla.redhat.com/" REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7" REDHAT_BUGZILLA_PRODUCT_VERSION=7.3 REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" REDHAT_SUPPORT_PRODUCT_VERSION="7.3" ================================================ FILE: grype/distro/testdata/os/rockylinux/etc/os-release ================================================ NAME="Rocky Linux" VERSION="8.4 (Green Obsidian)" ID="rocky" ID_LIKE="rhel fedora" VERSION_ID="8.4" PLATFORM_ID="platform:el8" PRETTY_NAME="Rocky Linux 8.4 (Green Obsidian)" ANSI_COLOR="0;32" CPE_NAME="cpe:/o:rocky:rocky:8.4:GA" HOME_URL="https://rockylinux.org/" BUG_REPORT_URL="https://bugs.rockylinux.org/" ROCKY_SUPPORT_PRODUCT="Rocky Linux" ROCKY_SUPPORT_PRODUCT_VERSION="8" ================================================ FILE: grype/distro/testdata/os/scientific/etc/os-release ================================================ NAME="Scientific Linux" VERSION="7.5 (Nitrogen)" ID="scientific" ID_LIKE="rhel centos fedora" VERSION_ID="7.5" PRETTY_NAME="Scientific Linux 7.5 (Nitrogen)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:scientificlinux:scientificlinux:7.5:GA" HOME_URL="http://www.scientificlinux.org//" BUG_REPORT_URL="mailto:scientific-linux-devel@listserv.fnal.gov" REDHAT_BUGZILLA_PRODUCT="Scientific Linux 7" REDHAT_BUGZILLA_PRODUCT_VERSION=7.5 REDHAT_SUPPORT_PRODUCT="Scientific Linux" REDHAT_SUPPORT_PRODUCT_VERSION="7.5" ================================================ FILE: grype/distro/testdata/os/scientific6/etc/redhat-release ================================================ Scientific Linux release 6.10 (Carbon) ================================================ FILE: grype/distro/testdata/os/secureos/etc/os-release ================================================ ID=secureos NAME="SecureOS" PRETTY_NAME="SecureOS (SecureBuild)" VERSION_ID="2025.09.09" HOME_URL="https://securebuild.com/" ================================================ FILE: grype/distro/testdata/os/sles/etc/os-release ================================================ NAME="SLES" VERSION="15-SP2" VERSION_ID="15.2" PRETTY_NAME="SUSE Linux Enterprise Server 15 SP2" ID="sles" ID_LIKE="suse" ANSI_COLOR="0;32" CPE_NAME="cpe:/o:suse:sles:15:sp2" DOCUMENTATION_URL="https://documentation.suse.com/" ================================================ FILE: grype/distro/testdata/os/ubuntu/etc/os-release ================================================ NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal ================================================ FILE: grype/distro/testdata/os/wolfi/etc/os-release ================================================ ID=wolfi NAME="Wolfi" PRETTY_NAME="Wolfi" VERSION_ID="20220914" HOME_URL="https://wolfi.dev" ================================================ FILE: grype/distro/testdata/partial-fields/missing-id/usr/lib/os-release ================================================ NAME="Debian GNU/Linux" VERSION_ID="8" ID_LIKE=debian ================================================ FILE: grype/distro/testdata/partial-fields/missing-version/usr/lib/os-release ================================================ NAME="Debian GNU/Linux" ID_LIKE=debian ================================================ FILE: grype/distro/testdata/partial-fields/unknown-id/usr/lib/os-release ================================================ NAME="Debian GNU/Linux" VERSION_ID="8" ID=my-awesome-distro ID_LIKE=debian ================================================ FILE: grype/distro/testdata/rhel-8 ================================================ NAME="Red Hat Enterprise Linux" VERSION="8.1 (Ootpa)" ID="rhel" ID_LIKE="fedora" VERSION_ID="8.1" PLATFORM_ID="platform:el8" PRETTY_NAME="Red Hat Enterprise Linux 8.1 (Ootpa)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:redhat:enterprise_linux:8.1:GA" HOME_URL="https://www.redhat.com/" BUG_REPORT_URL="https://bugzilla.redhat.com/" REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8" REDHAT_BUGZILLA_PRODUCT_VERSION=8.1 REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" REDHAT_SUPPORT_PRODUCT_VERSION="8.1" ================================================ FILE: grype/distro/testdata/ubuntu-20.04 ================================================ NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal ================================================ FILE: grype/distro/testdata/unprintable ================================================ PRETTY_NAME="Debian GNU/Linux 8 (jessie)" NAME="Debian GNU/Linux" VERSION_ID="8" VERSION="8 (jessie)" ID=debian HOME_URL="http://www.debian.org/" SUPPORT_URL="http://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ================================================ FILE: grype/distro/type.go ================================================ package distro import ( "github.com/anchore/syft/syft/linux" ) // Type represents the different Linux distribution options type Type string const ( // represents the set of supported Linux Distributions Debian Type = "debian" Ubuntu Type = "ubuntu" RedHat Type = "redhat" CentOS Type = "centos" Fedora Type = "fedora" Alpine Type = "alpine" Busybox Type = "busybox" AmazonLinux Type = "amazonlinux" OracleLinux Type = "oraclelinux" ArchLinux Type = "archlinux" OpenSuseLeap Type = "opensuseleap" SLES Type = "sles" Photon Type = "photon" Echo Type = "echo" Windows Type = "windows" Mariner Type = "mariner" Azure Type = "azurelinux" RockyLinux Type = "rockylinux" AlmaLinux Type = "almalinux" Gentoo Type = "gentoo" Wolfi Type = "wolfi" Chainguard Type = "chainguard" MinimOS Type = "minimos" Raspbian Type = "raspbian" Scientific Type = "scientific" SecureOS Type = "secureos" PostmarketOS Type = "postmarketos" ) // All contains all Linux distribution options var All = []Type{ Debian, Ubuntu, RedHat, CentOS, Fedora, Alpine, Busybox, AmazonLinux, OracleLinux, ArchLinux, OpenSuseLeap, SLES, Photon, Echo, Windows, Mariner, Azure, RockyLinux, AlmaLinux, Gentoo, Wolfi, Chainguard, MinimOS, Raspbian, Scientific, SecureOS, PostmarketOS, } // IDMapping maps a distro ID from the /etc/os-release (e.g. like "ubuntu") to a Distro type. var IDMapping = map[string]Type{ "debian": Debian, "ubuntu": Ubuntu, "rhel": RedHat, "centos": CentOS, "fedora": Fedora, "alpine": Alpine, "busybox": Busybox, "amzn": AmazonLinux, "ol": OracleLinux, "arch": ArchLinux, "opensuse-leap": OpenSuseLeap, "sles": SLES, "photon": Photon, "echo": Echo, "mariner": Mariner, "azurelinux": Azure, "rocky": RockyLinux, "almalinux": AlmaLinux, "gentoo": Gentoo, "wolfi": Wolfi, "chainguard": Chainguard, "minimos": MinimOS, "raspbian": Raspbian, "scientific": Scientific, "secureos": SecureOS, "postmarketos": PostmarketOS, } // aliasTypes maps common aliases to their corresponding Type. var aliasTypes = map[string]Type{ "Alpine Linux": Alpine, // needed for CPE matching (see #2039) "windows": Windows, "scientific linux": Scientific, // Scientific linux prior to v7 didn't have an os-release file and syft raises up "scientific linux" as the release id as parsed from /etc/redhat-release } var typeToIDMapping = map[Type]string{} func init() { for id, t := range IDMapping { if _, ok := typeToIDMapping[t]; ok { panic("duplicate Type found for ID: " + id + " with Type: " + string(t)) } typeToIDMapping[t] = id } } func TypeFromRelease(release linux.Release) Type { // first try the release ID if t, ok := IDMapping[release.ID]; ok { return t } if t, ok := aliasTypes[release.ID]; ok { return t } // use ID_LIKE as a backup for _, l := range release.IDLike { if t, ok := IDMapping[l]; ok { return t } if t, ok := aliasTypes[l]; ok { return t } } // then try the release name as a fallback if t, ok := IDMapping[release.Name]; ok { return t } if t, ok := aliasTypes[release.Name]; ok { return t } return "" } // String returns the string representation of the given Linux distribution. func (t Type) String() string { return string(t) } ================================================ FILE: grype/distro/type_test.go ================================================ package distro import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/syft/syft/linux" ) func TestTypeFromRelease(t *testing.T) { tests := []struct { name string release linux.Release want Type }{ { name: "direct ID mapping", release: linux.Release{ ID: "ubuntu", }, want: Ubuntu, }, { name: "direct ID mapping rhel", release: linux.Release{ ID: "rhel", }, want: RedHat, }, { name: "alias mapping", release: linux.Release{ ID: "Alpine Linux", }, want: Alpine, }, { name: "alias mapping windows", release: linux.Release{ ID: "windows", }, want: Windows, }, { name: "ID_LIKE mapping", release: linux.Release{ ID: "unknown-distro", IDLike: []string{"debian", "ubuntu"}, }, want: Debian, }, { name: "ID_LIKE alias mapping", release: linux.Release{ ID: "custom-alpine", IDLike: []string{"Alpine Linux"}, }, want: Alpine, }, { name: "fallback to name ID mapping", release: linux.Release{ ID: "unrecognized", Name: "fedora", }, want: Fedora, }, { name: "fallback to name alias mapping", release: linux.Release{ ID: "unrecognized", Name: "windows", }, want: Windows, }, { name: "empty result when no matches", release: linux.Release{ ID: "totally-unknown", Name: "also-unknown", }, want: "", }, { name: "prefer ID over ID_LIKE", release: linux.Release{ ID: "ubuntu", IDLike: []string{"debian"}, }, want: Ubuntu, }, { name: "prefer ID over name", release: linux.Release{ ID: "ubuntu", Name: "fedora", }, want: Ubuntu, }, { name: "prefer ID_LIKE over name", release: linux.Release{ ID: "unknown", IDLike: []string{"centos"}, Name: "fedora", }, want: CentOS, }, { name: "multiple ID_LIKE entries use first match", release: linux.Release{ ID: "unknown", IDLike: []string{"nonexistent", "alpine", "debian"}, }, want: Alpine, }, { name: "Scientific Linux 6", release: linux.Release{ Name: "Scientific Linux", ID: "scientific linux", IDLike: []string{"scientific linux"}, Version: "6.10 Carbon", VersionID: "6.10", }, want: Scientific, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := TypeFromRelease(tt.release) assert.Equal(t, tt.want, got) }) } } ================================================ FILE: grype/event/event.go ================================================ package event import ( "github.com/wagoodman/go-partybus" ) const ( typePrefix = "grype" cliTypePrefix = typePrefix + "-cli" // Events from the grype library UpdateVulnerabilityDatabase partybus.EventType = typePrefix + "-update-vulnerability-database" VulnerabilityScanningStarted partybus.EventType = typePrefix + "-vulnerability-scanning-started" DatabaseDiffingStarted partybus.EventType = typePrefix + "-database-diffing-started" // Events exclusively for the CLI // CLIAppUpdateAvailable is a partybus event that occurs when an application update is available CLIAppUpdateAvailable partybus.EventType = cliTypePrefix + "-app-update-available" // CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout CLIReport partybus.EventType = cliTypePrefix + "-report" // CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr CLINotification partybus.EventType = cliTypePrefix + "-notification" ) ================================================ FILE: grype/event/monitor/db_diff.go ================================================ package monitor import "github.com/wagoodman/go-progress" type DBDiff struct { Stager progress.Stager StageProgress progress.Progressable DifferencesDiscovered progress.Monitorable } ================================================ FILE: grype/event/monitor/matching.go ================================================ package monitor import ( "github.com/wagoodman/go-progress" "github.com/anchore/grype/grype/vulnerability" ) type Matching struct { PackagesProcessed progress.Progressable MatchesDiscovered progress.Monitorable Fixed progress.Monitorable Ignored progress.Monitorable Dropped progress.Monitorable BySeverity map[vulnerability.Severity]progress.Monitorable } ================================================ FILE: grype/event/parsers/parsers.go ================================================ package parsers import ( "fmt" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" ) type ErrBadPayload struct { Type partybus.EventType Field string Value interface{} } func (e *ErrBadPayload) Error() string { return fmt.Sprintf("event='%s' has bad event payload field='%v': '%+v'", string(e.Type), e.Field, e.Value) } func newPayloadErr(t partybus.EventType, field string, value interface{}) error { return &ErrBadPayload{ Type: t, Field: field, Value: value, } } func checkEventType(actual, expected partybus.EventType) error { if actual != expected { return newPayloadErr(expected, "Type", actual) } return nil } func ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgressable, error) { if err := checkEventType(e.Type, event.UpdateVulnerabilityDatabase); err != nil { return nil, err } prog, ok := e.Value.(progress.StagedProgressable) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } return prog, nil } func ParseVulnerabilityScanningStarted(e partybus.Event) (*monitor.Matching, error) { if err := checkEventType(e.Type, event.VulnerabilityScanningStarted); err != nil { return nil, err } mon, ok := e.Value.(monitor.Matching) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } return &mon, nil } func ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) { if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil { return nil, err } mon, ok := e.Value.(monitor.DBDiff) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } return &mon, nil } type UpdateCheck struct { New string Current string } func ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) { if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil { return nil, err } updateCheck, ok := e.Value.(UpdateCheck) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } return &updateCheck, nil } func ParseCLIReport(e partybus.Event) (string, string, error) { if err := checkEventType(e.Type, event.CLIReport); err != nil { return "", "", err } context, ok := e.Source.(string) if !ok { // this is optional context = "" } report, ok := e.Value.(string) if !ok { return "", "", newPayloadErr(e.Type, "Value", e.Value) } return context, report, nil } func ParseCLINotification(e partybus.Event) (string, string, error) { if err := checkEventType(e.Type, event.CLINotification); err != nil { return "", "", err } context, ok := e.Source.(string) if !ok { // this is optional context = "" } notification, ok := e.Value.(string) if !ok { return "", "", newPayloadErr(e.Type, "Value", e.Value) } return context, notification, nil } ================================================ FILE: grype/grypeerr/errors.go ================================================ package grypeerr var ( // ErrAboveSeverityThreshold indicates when a vulnerability severity is discovered that is equal // or above the given --fail-on severity value. ErrAboveSeverityThreshold = NewExpectedErr("discovered vulnerabilities at or above the severity threshold") // ErrDBUpgradeAvailable indicates that a DB upgrade is available. ErrDBUpgradeAvailable = NewExpectedErr("db upgrade available") ) ================================================ FILE: grype/grypeerr/expected_error.go ================================================ package grypeerr import ( "fmt" ) // ExpectedErr represents a class of expected errors that grype may produce. type ExpectedErr struct { Err error } // New generates a new ExpectedErr. func NewExpectedErr(msgFormat string, args ...interface{}) ExpectedErr { return ExpectedErr{ Err: fmt.Errorf(msgFormat, args...), } } // Error returns a string representing the underlying error condition. func (e ExpectedErr) Error() string { return e.Err.Error() } ================================================ FILE: grype/internal/generate.go ================================================ package internal //go:generate go run ./packagemetadata/generate/main.go ================================================ FILE: grype/internal/packagemetadata/discover_type_names.go ================================================ package packagemetadata import ( "fmt" "go/ast" "go/parser" "go/token" "os/exec" "path/filepath" "sort" "strings" "unicode" "github.com/scylladb/go-set/strset" ) var metadataExceptions = strset.New( "FileMetadata", "SBOMFileMetadata", "PURLLiteralMetadata", "CPELiteralMetadata", ) func DiscoverTypeNames() ([]string, error) { root, err := RepoRoot() if err != nil { return nil, err } files, err := filepath.Glob(filepath.Join(root, "grype/pkg/*.go")) if err != nil { return nil, err } return findMetadataDefinitionNames(files...) } func RepoRoot() (string, error) { root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { return "", fmt.Errorf("unable to find repo root dir: %+v", err) } absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) if err != nil { return "", fmt.Errorf("unable to get abs path to repo root: %w", err) } return absRepoRoot, nil } func findMetadataDefinitionNames(paths ...string) ([]string, error) { names := strset.New() usedNames := strset.New() for _, path := range paths { metadataDefinitions, usedTypeNames, err := findMetadataDefinitionNamesInFile(path) if err != nil { return nil, err } // useful for debugging... // fmt.Println(path) // fmt.Println("Defs:", metadataDefinitions) // fmt.Println("Used Types:", usedTypeNames) // fmt.Println() names.Add(metadataDefinitions...) usedNames.Add(usedTypeNames...) } // any definition that is used within another struct should not be considered a top-level metadata definition names.Remove(usedNames.List()...) strNames := names.List() sort.Strings(strNames) // note: 3 is a point-in-time gut check. This number could be updated if new metadata definitions are added, but is not required. // it is really intended to catch any major issues with the generation process that would generate, say, 0 definitions. if len(strNames) < 3 { return nil, fmt.Errorf("not enough metadata definitions found: discovered %d ", len(strNames)) } return strNames, nil } func findMetadataDefinitionNamesInFile(path string) ([]string, []string, error) { // set up the parser fs := token.NewFileSet() f, err := parser.ParseFile(fs, path, nil, parser.ParseComments) if err != nil { return nil, nil, err } var metadataDefinitions []string var usedTypeNames []string for _, decl := range f.Decls { // check if the declaration is a type declaration spec, ok := decl.(*ast.GenDecl) if !ok || spec.Tok != token.TYPE { continue } // loop over all types declared in the type declaration for _, typ := range spec.Specs { // check if the type is a struct type spec, ok := typ.(*ast.TypeSpec) if !ok || spec.Type == nil { continue } structType, ok := spec.Type.(*ast.StructType) if !ok { continue } // check if the struct type ends with "Metadata" name := spec.Name.String() // only look for exported types that end with "Metadata" if isMetadataTypeCandidate(name) { // print the full declaration of the struct type metadataDefinitions = append(metadataDefinitions, name) usedTypeNames = append(usedTypeNames, typeNamesUsedInStruct(structType)...) } } } return metadataDefinitions, usedTypeNames, nil } func typeNamesUsedInStruct(structType *ast.StructType) []string { // recursively find all type names used in the struct type var names []string for i := range structType.Fields.List { // capture names of all of the types (not field names) ast.Inspect(structType.Fields.List[i].Type, func(n ast.Node) bool { ident, ok := n.(*ast.Ident) if !ok { return true } // add the type name to the list names = append(names, ident.Name) // continue inspecting return true }) } return names } func isMetadataTypeCandidate(name string) bool { return len(name) > 0 && strings.HasSuffix(name, "Metadata") && unicode.IsUpper(rune(name[0])) && // must be exported !metadataExceptions.Has(name) } ================================================ FILE: grype/internal/packagemetadata/generate/main.go ================================================ package main import ( "fmt" "os" "github.com/dave/jennifer/jen" "github.com/anchore/grype/grype/internal/packagemetadata" ) // This program is invoked from grype/internal and generates packagemetadata/generated.go const ( pkgImport = "github.com/anchore/grype/grype/pkg" path = "packagemetadata/generated.go" ) func main() { typeNames, err := packagemetadata.DiscoverTypeNames() if err != nil { panic(fmt.Errorf("unable to get all metadata type names: %w", err)) } fmt.Printf("updating package metadata type list with %+v types\n", len(typeNames)) f := jen.NewFile("packagemetadata") f.HeaderComment("DO NOT EDIT: generated by grype/internal/packagemetadata/generate/main.go") f.ImportName(pkgImport, "pkg") f.Comment("AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field).") f.Func().Id("AllTypes").Params().Index().Any().BlockFunc(func(g *jen.Group) { g.ReturnFunc(func(g *jen.Group) { g.Index().Any().ValuesFunc(func(g *jen.Group) { for _, typeName := range typeNames { g.Qual(pkgImport, typeName).Values() } }) }) }) rendered := fmt.Sprintf("%#v", f) fh, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { panic(fmt.Errorf("unable to open file: %w", err)) } _, err = fh.WriteString(rendered) if err != nil { panic(fmt.Errorf("unable to write file: %w", err)) } if err := fh.Close(); err != nil { panic(fmt.Errorf("unable to close file: %w", err)) } } ================================================ FILE: grype/internal/packagemetadata/generated.go ================================================ // DO NOT EDIT: generated by grype/internal/packagemetadata/generate/main.go package packagemetadata import "github.com/anchore/grype/grype/pkg" // AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field). func AllTypes() []any { return []any{pkg.ApkMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.GolangSourceMetadata{}, pkg.JavaMetadata{}, pkg.JavaVMInstallationMetadata{}, pkg.RpmMetadata{}} } ================================================ FILE: grype/internal/packagemetadata/names.go ================================================ package packagemetadata import ( "reflect" "sort" "strings" "github.com/anchore/grype/grype/pkg" ) // jsonNameFromType is a map of all known package metadata types to their current JSON name and all previously known aliases. // TODO: in the future the metadata type names should match how it is used in syft. However, since the data shapes are // not the same it may be important to select different names. This design decision has been deferred, for now // the same metadata types that have been used in the past should be used here. var jsonNameFromType = map[reflect.Type][]string{ reflect.TypeOf(pkg.ApkMetadata{}): nameList("ApkMetadata"), reflect.TypeOf(pkg.GolangBinMetadata{}): nameList("GolangBinMetadata"), reflect.TypeOf(pkg.GolangModMetadata{}): nameList("GolangModMetadata"), reflect.TypeOf(pkg.GolangSourceMetadata{}): nameList("GolangSourceMetadata"), reflect.TypeOf(pkg.JavaMetadata{}): nameList("JavaMetadata"), reflect.TypeOf(pkg.RpmMetadata{}): nameList("RpmMetadata"), reflect.TypeOf(pkg.JavaVMInstallationMetadata{}): nameList("JavaVMInstallationMetadata"), } //nolint:unparam func nameList(id string, others ...string) []string { names := []string{id} for _, o := range others { names = append(names, expandLegacyNameVariants(o)...) } return names } func expandLegacyNameVariants(name string) []string { candidates := []string{name} if strings.HasSuffix(name, "MetadataType") { candidates = append(candidates, strings.TrimSuffix(name, "Type")) } else if strings.HasSuffix(name, "Metadata") { candidates = append(candidates, name+"Type") } return candidates } func AllTypeNames() []string { names := make([]string, 0) for _, t := range AllTypes() { names = append(names, reflect.TypeOf(t).Name()) } return names } func JSONName(metadata any) string { if vs, exists := jsonNameFromType[reflect.TypeOf(metadata)]; exists { return vs[0] } return "" } func ReflectTypeFromJSONName(name string) reflect.Type { name = strings.ToLower(name) for _, t := range sortedTypes(jsonNameFromType) { vs := jsonNameFromType[t] for _, v := range vs { if strings.ToLower(v) == name { return t } } } return nil } func sortedTypes(typeNameMapping map[reflect.Type][]string) []reflect.Type { types := make([]reflect.Type, 0) for t := range typeNameMapping { types = append(types, t) } // sort the types by their first JSON name sort.Slice(types, func(i, j int) bool { return typeNameMapping[types[i]][0] < typeNameMapping[types[j]][0] }) return types } ================================================ FILE: grype/internal/packagemetadata/names_test.go ================================================ package packagemetadata import ( "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/pkg" ) func TestAllNames(t *testing.T) { // note: this is a form of completion testing relative to the current code base. expected, err := DiscoverTypeNames() require.NoError(t, err) actual := AllTypeNames() // ensure that the codebase (from ast analysis) reflects the latest code generated state if !assert.ElementsMatch(t, expected, actual) { t.Errorf("metadata types not fully represented: \n%s", cmp.Diff(expected, actual)) t.Log("did you add a new pkg.*Metadata type without updating the JSON schema?") t.Log("if so, you need to update the schema version and regenerate the JSON schema (make generate-json-schema)") } for _, ty := range AllTypes() { assert.NotEmpty(t, JSONName(ty), "metadata type %q does not have a JSON name", ty) } } func TestReflectTypeFromJSONName(t *testing.T) { tests := []struct { name string lookup string wantRecord reflect.Type }{ { name: "GolangBinMetadata lookup", lookup: "GolangBinMetadata", wantRecord: reflect.TypeOf(pkg.GolangBinMetadata{}), }, { name: "GolangModMetadata lookup", lookup: "GolangModMetadata", wantRecord: reflect.TypeOf(pkg.GolangModMetadata{}), }, { name: "JavaMetadata lookup", lookup: "JavaMetadata", wantRecord: reflect.TypeOf(pkg.JavaMetadata{}), }, { name: "RpmMetadata lookup", lookup: "RpmMetadata", wantRecord: reflect.TypeOf(pkg.RpmMetadata{}), }, { name: "JavaVMInstallationMetadata lookup", lookup: "JavaVMInstallationMetadata", wantRecord: reflect.TypeOf(pkg.JavaVMInstallationMetadata{}), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ReflectTypeFromJSONName(tt.lookup) assert.Equal(t, tt.wantRecord.Name(), got.Name()) }) } } ================================================ FILE: grype/lib.go ================================================ package grype import ( "github.com/wagoodman/go-partybus" "github.com/anchore/go-logger" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" ) func SetLogger(l logger.Logger) { log.Set(l) } func SetBus(b *partybus.Bus) { bus.Set(b) } ================================================ FILE: grype/load_vulnerability_db.go ================================================ package grype import ( "fmt" v6 "github.com/anchore/grype/grype/db/v6" v6dist "github.com/anchore/grype/grype/db/v6/distribution" v6inst "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) func LoadVulnerabilityDB(distCfg v6dist.Config, installCfg v6inst.Config, update bool) (vulnerability.Provider, *vulnerability.ProviderStatus, error) { client, err := v6dist.NewClient(distCfg) if err != nil { return nil, nil, fmt.Errorf("unable to create distribution client: %w", err) } c, err := v6inst.NewCurator(installCfg, client) if err != nil { return nil, nil, fmt.Errorf("unable to create curator: %w", err) } if update { updated, err := c.Update() if err != nil { if distCfg.RequireUpdateCheck { return nil, nil, fmt.Errorf("unable to update db: %w", err) } log.WithFields("error", err).Warn("error updating db") } if !updated { log.Debug("no db update found") } } else { log.Debug("skipping db update") } s := c.Status() if s.Error != nil { return nil, nil, s.Error } rdr, err := c.Reader() if err != nil { return nil, nil, fmt.Errorf("unable to create db reader: %w", err) } return v6.NewVulnerabilityProvider(rdr), &s, nil } ================================================ FILE: grype/load_vulnerability_db_bench_test.go ================================================ package grype import ( "math" "path/filepath" "testing" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" ) // this benchmark was added to measure the performance // of LoadVulnerabilityDB, specifically regarding hash validation. // https://github.com/anchore/grype/issues/1502 func BenchmarkLoadVulnerabilityDB(b *testing.B) { for range b.N { _, _, err := LoadVulnerabilityDB(distribution.Config{ LatestURL: distribution.DefaultConfig().LatestURL, }, installation.Config{ DBRootDir: filepath.Join(".tmp", "grype-db"), ValidateAge: false, ValidateChecksum: true, MaxAllowedBuiltAge: math.MaxInt32, UpdateCheckMaxFrequency: math.MaxInt32, }, true) if err != nil { b.Fatal(err) } } } ================================================ FILE: grype/match/details.go ================================================ package match import ( "fmt" "strings" "github.com/gohugoio/hashstructure" ) type Details []Detail type Detail struct { Type Type // The kind of match made (an exact match, fuzzy match, indirect vs direct, etc). SearchedBy interface{} // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made. Found interface{} // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within. Matcher MatcherType // The matcher object that discovered the match. Confidence float64 // The certainty of the match as a ratio (currently unused, reserved for future use). } // String is the string representation of select match fields. func (m Detail) String() string { return fmt.Sprintf("Detail(searchedBy=%q found=%q matcher=%q)", m.SearchedBy, m.Found, m.Matcher) } func (m Details) Matchers() (tys []MatcherType) { if len(m) == 0 { return nil } for _, d := range m { tys = append(tys, d.Matcher) } return tys } func (m Details) Types() (tys []Type) { if len(m) == 0 { return nil } for _, d := range m { tys = append(tys, d.Type) } return tys } func (m Detail) ID() string { f, err := hashstructure.Hash(&m, &hashstructure.HashOptions{ ZeroNil: true, SlicesAsSets: true, }) if err != nil { return "" } return fmt.Sprintf("%x", f) } func (m Details) Len() int { return len(m) } func (m Details) Less(i, j int) bool { a := m[i] b := m[j] if a.Type != b.Type { // exact-direct-match < exact-indirect-match < cpe-match at := typeOrder[a.Type] bt := typeOrder[b.Type] if at == 0 { return false } else if bt == 0 { return true } return at < bt } // sort by confidence if a.Confidence != b.Confidence { // flipped comparison since we want higher confidence to be first return a.Confidence > b.Confidence } // if the types are the same, then sort by the ID (costly, but deterministic) return strings.Compare(a.ID(), b.ID()) < 0 } func (m Details) Swap(i, j int) { m[i], m[j] = m[j], m[i] } ================================================ FILE: grype/match/details_test.go ================================================ package match import ( "sort" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDetails_Sorting(t *testing.T) { detailExactDirectHigh := Detail{ Type: ExactDirectMatch, Confidence: 0.9, SearchedBy: "attribute1", Found: "value1", Matcher: "matcher1", } detailExactDirectLow := Detail{ Type: ExactDirectMatch, Confidence: 0.5, SearchedBy: "attribute1", Found: "value1", Matcher: "matcher1", } detailExactIndirect := Detail{ Type: ExactIndirectMatch, Confidence: 0.7, SearchedBy: "attribute2", Found: "value2", Matcher: "matcher2", } detailCPEMatch := Detail{ Type: CPEMatch, Confidence: 0.8, SearchedBy: "attribute3", Found: "value3", Matcher: "matcher3", } tests := []struct { name string details Details expected Details }{ { name: "sorts by type first, then by confidence", details: Details{ detailCPEMatch, detailExactDirectHigh, detailExactIndirect, detailExactDirectLow, }, expected: Details{ detailExactDirectHigh, detailExactDirectLow, detailExactIndirect, detailCPEMatch, }, }, { name: "sorts by confidence within the same type", details: Details{ detailExactDirectLow, detailExactDirectHigh, }, expected: Details{ detailExactDirectHigh, detailExactDirectLow, }, }, { name: "sorts by ID when type and confidence are the same", details: Details{ // clone of detailExactDirectLow with slight difference to enforce ID sorting { Type: ExactDirectMatch, Confidence: 0.5, SearchedBy: "attribute2", Found: "value2", Matcher: "matcher2", }, detailExactDirectLow, }, expected: Details{ detailExactDirectLow, { Type: ExactDirectMatch, Confidence: 0.5, SearchedBy: "attribute2", Found: "value2", Matcher: "matcher2", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sort.Sort(tt.details) require.Equal(t, tt.expected, tt.details) }) } } func TestHasExclusivelyAnyMatchTypes(t *testing.T) { tests := []struct { name string details Details types []Type expected bool }{ { name: "all types allowed", details: Details{{Type: "A"}, {Type: "B"}}, types: []Type{"A", "B"}, expected: true, }, { name: "mixed types with disallowed", details: Details{{Type: "A"}, {Type: "B"}, {Type: "C"}}, types: []Type{"A", "B"}, expected: false, }, { name: "single allowed type", details: Details{{Type: "A"}}, types: []Type{"A"}, expected: true, }, { name: "empty details", details: Details{}, types: []Type{"A"}, expected: false, }, { name: "empty types list", details: Details{{Type: "A"}}, types: []Type{}, expected: false, }, { name: "no match with disallowed type", details: Details{{Type: "C"}}, types: []Type{"A", "B"}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := hasExclusivelyAnyMatchTypes(tt.details, tt.types...) assert.Equal(t, tt.expected, result) }) } } ================================================ FILE: grype/match/explicit_ignores.go ================================================ package match import ( "github.com/anchore/grype/internal/log" ) var explicitIgnoreRules []IgnoreRule func init() { type ignoreValues struct { typ string vulnerabilities []string packages []string } var explicitIgnores = []ignoreValues{ // Based on https://github.com/anchore/grype/issues/552, which includes a reference to the // https://github.com/mergebase/log4j-samples collection, we want to filter these explicitly: { typ: "java-archive", vulnerabilities: []string{"CVE-2021-44228", "CVE-2021-45046", "GHSA-jfh8-c2jp-5v3q", "GHSA-7rjr-3q55-vv33", "CVE-2020-9493", "CVE-2022-23307", "CVE-2023-26464"}, packages: []string{"log4j-api", "log4j-slf4j-impl", "log4j-to-slf4j", "log4j-1.2-api", "log4j-detector", "log4j-over-slf4j", "slf4j-log4j12"}, }, // Based on https://github.com/anchore/grype/issues/558: { typ: "go-module", vulnerabilities: []string{"CVE-2015-5237", "CVE-2021-22570"}, packages: []string{"google.golang.org/protobuf"}, }, // Affects Squiz Matrix, not in any way related to the matrix ruby gem { typ: "gem", vulnerabilities: []string{"CVE-2017-14196", "CVE-2017-14197", "CVE-2017-14198", "CVE-2019-19373", "CVE-2019-19374"}, packages: []string{"matrix"}, }, // Affects the DeleGate proxy server, not in any way related to the delegate ruby gem { typ: "gem", vulnerabilities: []string{"CVE-1999-1338", "CVE-2001-1202", "CVE-2002-1781", "CVE-2004-0789", "CVE-2004-2003", "CVE-2005-0036", "CVE-2005-0861", "CVE-2006-2072", "CVE-2015-7556"}, packages: []string{"delegate"}, }, // Affects the Observer autodiscovery PHP/MySQL/SNMP/CDP based network management system, not in any way related to the observer ruby gem { typ: "gem", vulnerabilities: []string{"CVE-2008-4318"}, packages: []string{"observer"}, }, // Affects the WeeChat logger plugin, not in any way related to the logger ruby gem { typ: "gem", vulnerabilities: []string{"CVE-2017-14727"}, packages: []string{"logger"}, }, // https://github.com/anchore/grype/issues/2412#issuecomment-2663656195 { typ: "deb", vulnerabilities: []string{"CVE-2023-45853"}, packages: []string{"zlib1g", "zlib"}, }, } for _, ignore := range explicitIgnores { for _, vulnerability := range ignore.vulnerabilities { for _, packageName := range ignore.packages { explicitIgnoreRules = append(explicitIgnoreRules, IgnoreRule{ Vulnerability: vulnerability, Package: IgnoreRulePackage{ Name: packageName, Type: ignore.typ, }, }) } } } } // ApplyExplicitIgnoreRules Filters out matches meeting the criteria defined above and those within the grype database func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) (Matches, []IgnoredMatch) { var ignoreRules []IgnoreRule ignoreRules = append(ignoreRules, explicitIgnoreRules...) if provider != nil { for _, m := range matches.Sorted() { r, err := provider.IgnoreRules(m.Vulnerability.ID) if err != nil { log.Warnf("unable to get ignore rules for vuln id=%s", m.Vulnerability.ID) continue } ignoreRules = append(ignoreRules, r...) } } return ApplyIgnoreRules(matches, ignoreRules) } ================================================ FILE: grype/match/explicit_ignores_test.go ================================================ package match import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type mockExclusionProvider struct { data map[string][]IgnoreRule } func newMockExclusionProvider() *mockExclusionProvider { d := mockExclusionProvider{ data: make(map[string][]IgnoreRule), } d.stub() return &d } func (d *mockExclusionProvider) stub() { } func (d *mockExclusionProvider) IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error) { return d.data[vulnerabilityID], nil } func Test_ApplyExplicitIgnoreRules(t *testing.T) { type cvePkg struct { cve string pkg string } tests := []struct { name string typ syftPkg.Type matches []cvePkg expected []string ignored []string }{ // some explicit log4j-related data: // "CVE-2021-44228", "CVE-2021-45046", "GHSA-jfh8-c2jp-5v3q", "GHSA-7rjr-3q55-vv33", // "log4j-api", "log4j-slf4j-impl", "log4j-to-slf4j", "log4j-1.2-api", { name: "keeps non-matching packages", typ: "java-archive", matches: []cvePkg{ {"CVE-2021-44228", "log4j-core"}, {"CVE-2021-43452", "foo-tool"}, }, expected: []string{"log4j-core", "foo-tool"}, }, { name: "keeps non-matching CVEs", typ: "java-archive", matches: []cvePkg{ {"CVE-2021-428", "log4j-api"}, {"CVE-2021-43452", "foo-tool"}, }, expected: []string{"log4j-api", "foo-tool"}, }, { name: "filters only matching CVE and package", typ: "java-archive", matches: []cvePkg{ {"CVE-2021-44228", "log4j-api"}, {"CVE-2021-44228", "log4j-core"}, }, expected: []string{"log4j-core"}, ignored: []string{"log4j-api"}, }, { name: "filters all matching CVEs and packages", typ: "java-archive", matches: []cvePkg{ {"GHSA-jfh8-c2jp-5v3q", "log4j-api"}, {"GHSA-jfh8-c2jp-5v3q", "log4j-slf4j-impl"}, }, expected: []string{}, ignored: []string{"log4j-api", "log4j-slf4j-impl"}, }, { name: "filters invalid CVEs for protobuf Go module", typ: "go-module", matches: []cvePkg{ {"CVE-2015-5237", "google.golang.org/protobuf"}, {"CVE-2021-22570", "google.golang.org/protobuf"}, }, expected: []string{}, ignored: []string{"google.golang.org/protobuf", "google.golang.org/protobuf"}, }, { name: "keeps valid CVEs for protobuf Go module", typ: "go-module", matches: []cvePkg{ {"CVE-1998-99999", "google.golang.org/protobuf"}, }, expected: []string{"google.golang.org/protobuf"}, }, } p := newMockExclusionProvider() for _, test := range tests { t.Run(test.name, func(t *testing.T) { matches := NewMatches() for _, cp := range test.matches { matches.Add(Match{ Package: pkg.Package{ ID: pkg.ID(cp.pkg), Name: cp.pkg, Type: test.typ, }, Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: cp.cve}, }, }) } filtered, ignores := ApplyExplicitIgnoreRules(p, matches) var found []string for match := range filtered.Enumerate() { found = append(found, match.Package.Name) } assert.ElementsMatch(t, test.expected, found) if len(test.ignored) > 0 { var ignored []string for _, i := range ignores { ignored = append(ignored, i.Package.Name) } assert.ElementsMatch(t, test.ignored, ignored) } else { assert.Empty(t, ignores) } }) } } ================================================ FILE: grype/match/fingerprint.go ================================================ package match import ( "fmt" "github.com/gohugoio/hashstructure" "github.com/anchore/grype/grype/pkg" ) type Fingerprint struct { coreFingerprint vulnerabilityFixes string } type coreFingerprint struct { vulnerabilityID string vulnerabilityNamespace string packageID pkg.ID // note: this encodes package name, version, type, location } func (m Fingerprint) String() string { return fmt.Sprintf("Fingerprint(vuln=%q namespace=%q fixes=%q package=%q)", m.vulnerabilityID, m.vulnerabilityNamespace, m.vulnerabilityFixes, m.packageID) } func (m Fingerprint) ID() string { f, err := hashstructure.Hash(&m, &hashstructure.HashOptions{ ZeroNil: true, SlicesAsSets: true, }) if err != nil { return "" } return fmt.Sprintf("%x", f) } ================================================ FILE: grype/match/ignore.go ================================================ package match import ( "regexp" "github.com/bmatcuk/doublestar/v2" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) // IgnoreFilter implementations are used to filter matches, returning all applicable IgnoreRule(s) that applied, // these could include an IgnoreRule with only a Reason value filled in for synthetically generated rules type IgnoreFilter interface { IgnoreMatch(match Match) []IgnoreRule } // An IgnoredMatch is a vulnerability Match that has been ignored because one or more IgnoreRules applied to the match. type IgnoredMatch struct { Match // AppliedIgnoreRules are the rules that were applied to the match that caused Grype to ignore it. AppliedIgnoreRules []IgnoreRule } // An IgnoreRule specifies criteria for a vulnerability match to meet in order // to be ignored. Not all criteria (fields) need to be specified, but all // specified criteria must be met by the vulnerability match in order for the // rule to apply. type IgnoreRule struct { Vulnerability string `yaml:"vulnerability" json:"vulnerability" mapstructure:"vulnerability"` IncludeAliases bool `yaml:"include-aliases" json:"include-aliases" mapstructure:"include-aliases"` Reason string `yaml:"reason" json:"reason" mapstructure:"reason"` Namespace string `yaml:"namespace" json:"namespace" mapstructure:"namespace"` FixState string `yaml:"fix-state" json:"fix-state" mapstructure:"fix-state"` Package IgnoreRulePackage `yaml:"package" json:"package" mapstructure:"package"` VexStatus string `yaml:"vex-status" json:"vex-status" mapstructure:"vex-status"` VexJustification string `yaml:"vex-justification" json:"vex-justification" mapstructure:"vex-justification"` MatchType Type `yaml:"match-type" json:"match-type" mapstructure:"match-type"` } // IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule. type IgnoreRulePackage struct { Name string `yaml:"name" json:"name" mapstructure:"name"` Version string `yaml:"version" json:"version" mapstructure:"version"` Language string `yaml:"language" json:"language" mapstructure:"language"` Type string `yaml:"type" json:"type" mapstructure:"type"` Location string `yaml:"location" json:"location" mapstructure:"location"` UpstreamName string `yaml:"upstream-name" json:"upstream-name" mapstructure:"upstream-name"` } // ApplyIgnoreRules iterates through the provided matches and, for each match, // determines if the match should be ignored, by evaluating if any of the // provided IgnoreRules apply to the match. If any rules apply to the match, all // applicable rules are attached to the Match to form an IgnoredMatch. // ApplyIgnoreRules returns two collections: the matches that are not being // ignored, and the matches that are being ignored. func ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMatch) { matched, ignored := ApplyIgnoreFilters(matches.Sorted(), rules...) return NewMatches(matched...), ignored } // ApplyIgnoreFilters applies all the IgnoreFilter(s) to the provided set of matches, // splitting the results into a set of matched matches and ignored matches func ApplyIgnoreFilters[T IgnoreFilter](matches []Match, filters ...T) ([]Match, []IgnoredMatch) { var out []Match var ignoredMatches []IgnoredMatch for _, match := range matches { var applicableRules []IgnoreRule for _, filter := range filters { applicableRules = append(applicableRules, filter.IgnoreMatch(match)...) } if len(applicableRules) > 0 { ignoredMatches = append(ignoredMatches, IgnoredMatch{ Match: match, AppliedIgnoreRules: applicableRules, }) continue } out = append(out, match) } return out, ignoredMatches } func (r IgnoreRule) IgnoreMatch(match Match) []IgnoreRule { // VEX rules are handled by the vex processor if r.VexStatus != "" { return nil } ignoreConditions := getIgnoreConditionsForRule(r) if len(ignoreConditions) == 0 { // this rule specifies no criteria, so it doesn't apply to the Match return nil } for _, condition := range ignoreConditions { if !condition(match) { // as soon as one rule criterion doesn't apply, we know this rule doesn't apply to the Match return nil } } // all criteria specified in the rule apply to this Match return []IgnoreRule{r} } // HasConditions returns true if the ignore rule has conditions // that can cause a match to be ignored func (r IgnoreRule) HasConditions() bool { return len(getIgnoreConditionsForRule(r)) == 0 } // ignoreFilters implements match.IgnoreFilter on a slice of objects that implement the same interface type ignoreFilters[T IgnoreFilter] []T func (r ignoreFilters[T]) IgnoreMatch(match Match) []IgnoreRule { for _, rule := range r { ignores := rule.IgnoreMatch(match) if len(ignores) > 0 { return ignores } } return nil } var _ IgnoreFilter = (*ignoreFilters[IgnoreRule])(nil) // An ignoreCondition is a function that returns a boolean indicating whether // the given Match should be ignored. type ignoreCondition func(match Match) bool func getIgnoreConditionsForRule(rule IgnoreRule) []ignoreCondition { var ignoreConditions []ignoreCondition if v := rule.Vulnerability; v != "" { ignoreConditions = append(ignoreConditions, ifVulnerabilityApplies(v, rule.IncludeAliases)) } if ns := rule.Namespace; ns != "" { ignoreConditions = append(ignoreConditions, ifNamespaceApplies(ns)) } if n := rule.Package.Name; n != "" { ignoreConditions = append(ignoreConditions, ifPackageNameApplies(n)) } if v := rule.Package.Version; v != "" { ignoreConditions = append(ignoreConditions, ifPackageVersionApplies(v)) } if l := rule.Package.Language; l != "" { ignoreConditions = append(ignoreConditions, ifPackageLanguageApplies(l)) } if t := rule.Package.Type; t != "" { ignoreConditions = append(ignoreConditions, ifPackageTypeApplies(t)) } if l := rule.Package.Location; l != "" { ignoreConditions = append(ignoreConditions, ifPackageLocationApplies(l)) } if fs := rule.FixState; fs != "" { ignoreConditions = append(ignoreConditions, ifFixStateApplies(fs)) } if upstreamName := rule.Package.UpstreamName; upstreamName != "" { ignoreConditions = append(ignoreConditions, ifUpstreamPackageNameApplies(upstreamName)) } if matchType := rule.MatchType; matchType != "" { ignoreConditions = append(ignoreConditions, ifMatchTypeApplies(matchType)) } return ignoreConditions } func ifFixStateApplies(fs string) ignoreCondition { return func(match Match) bool { if fs == string(vulnerability.FixStateUnknown) && match.Vulnerability.Fix.State == "" { // no fix state specified is effectively "unknown" return true } return fs == string(match.Vulnerability.Fix.State) } } func ifVulnerabilityApplies(vulnerability string, includeAliases bool) ignoreCondition { return func(match Match) bool { if vulnerability == match.Vulnerability.ID { return true } if includeAliases { for _, related := range match.Vulnerability.RelatedVulnerabilities { if vulnerability == related.ID { return true } } } return false } } func ifNamespaceApplies(namespace string) ignoreCondition { return func(match Match) bool { return namespace == match.Vulnerability.Namespace } } func packageNameRegex(packageName string) (*regexp.Regexp, error) { pattern := packageName if packageName[0] != '$' || packageName[len(packageName)-1] != '^' { pattern = "^" + packageName + "$" } return regexp.Compile(pattern) } func ifPackageNameApplies(name string) ignoreCondition { // with enough ignore rules, we could end up needlessly creating a lot of regexes, which is not ideal. // instead lets detect if the input string is a regex or not, and if it is, then compile it... // otherwise, we can just do a simple string comparison if isLikelyARegex(name) { pattern, err := packageNameRegex(name) if err != nil || pattern == nil { return func(Match) bool { return false } } return func(match Match) bool { return pattern.MatchString(match.Package.Name) } } return func(match Match) bool { return name == match.Package.Name } } func ifPackageVersionApplies(version string) ignoreCondition { // TODO I think we will might need to add the metadata compare logic here return func(match Match) bool { return version == match.Package.Version } } func ifPackageLanguageApplies(language string) ignoreCondition { return func(match Match) bool { return language == string(match.Package.Language) } } func ifPackageTypeApplies(t string) ignoreCondition { return func(match Match) bool { return t == string(match.Package.Type) } } func ifPackageLocationApplies(location string) ignoreCondition { return func(match Match) bool { return ruleLocationAppliesToMatch(location, match) } } func ifUpstreamPackageNameApplies(name string) ignoreCondition { // with enough ignore rules, we could end up needlessly creating a lot of regexes, which is not ideal. // instead lets detect if the input string is a regex or not, and if it is, then compile it... // otherwise, we can just do a simple string comparison if isLikelyARegex(name) { pattern, err := packageNameRegex(name) if err != nil { log.WithFields("name", name, "error", err).Debug("unable to parse name expression") return func(Match) bool { return false } } return func(match Match) bool { for _, upstream := range match.Package.Upstreams { if pattern.MatchString(upstream.Name) { return true } } return false } } return func(match Match) bool { for _, upstream := range match.Package.Upstreams { if name == upstream.Name { return true } } return false } } // isRegexPattern is a compiled regex that matches common regex characters. We intentionally leave out // the '.' character, as it is a common character in package names and versions, and we do not want to // treat it as a regex unless there is other evidence that it is a regex. var isRegexPattern = regexp.MustCompile(`[\^\$\*\+\?\[\]\(\)\{\}\|\\]|\\[dDwWsSnrtfv]`) func isLikelyARegex(s string) bool { return isRegexPattern.MatchString(s) } func ifMatchTypeApplies(matchType Type) ignoreCondition { return func(match Match) bool { for _, mType := range match.Details.Types() { if mType == matchType { return true } } return false } } func ruleLocationAppliesToMatch(location string, match Match) bool { for _, packageLocation := range match.Package.Locations.ToSlice() { if ruleLocationAppliesToPath(location, packageLocation.RealPath) { return true } if ruleLocationAppliesToPath(location, packageLocation.AccessPath) { return true } } return false } func ruleLocationAppliesToPath(location, path string) bool { doesMatch, err := doublestar.Match(location, path) if err != nil { return false } return doesMatch } ================================================ FILE: grype/match/ignore_test.go ================================================ package match import ( "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" ) var ( allMatches = []Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-123", Namespace: "debian-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "dive", Version: "0.5.2", Type: "deb", Locations: file.NewLocationSet(file.NewLocation("/path/that/has/dive")), }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-456", Namespace: "ruby-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateNotFixed, }, RelatedVulnerabilities: []vulnerability.Reference{ { ID: "CVE-123", }, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "reach", Version: "100.0.50", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/reach", "/virtual/path/that/has/reach")), }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-457", Namespace: "ruby-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateWontFix, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "beach", Version: "100.0.51", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/beach", "/virtual/path/that/has/beach")), }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-458", Namespace: "ruby-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "speach", Version: "100.0.52", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/speach", "/virtual/path/that/has/speach")), }, }, } // For testing the match-type rules matchTypesMatches = []Match{ // Direct match, not like a normal kernel header match { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1", Namespace: "fake-redhat-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kernel-headers1", Version: "5.1.0", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "kernel2"}, }, }, Details: []Detail{ { Type: ExactDirectMatch, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2", Namespace: "fake-deb-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kernel-headers2", Version: "5.1.0", Type: syftPkg.DebPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "kernel2"}, }, }, Details: []Detail{ { Type: ExactIndirectMatch, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1", Namespace: "npm-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "npm1", Version: "5.1.0", Type: syftPkg.NpmPkg, }, Details: []Detail{ { Type: CPEMatch, }, }, }, } // For testing the match-type and upstream ignore rules kernelHeadersMatches = []Match{ // RPM-like match similar to what we see from RedHat { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2", Namespace: "fake-redhat-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kernel-headers", Version: "5.1.0", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "kernel"}, }, }, Details: []Detail{ { Type: ExactIndirectMatch, }, }, }, // debian-like match, showing the kernel header package name w/embedded version { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2", Namespace: "fake-debian-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "linux-headers-5.2.0", Version: "5.2.1", Type: syftPkg.DebPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "linux"}, }, }, Details: []Detail{ { Type: ExactIndirectMatch, }, }, }, // linux-like match, similar to what we see from debian\ubuntu { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-3", Namespace: "fake-linux-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "linux-azure-headers-generic", Version: "5.2.1", Type: syftPkg.DebPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "linux-azure"}, }, }, Details: []Detail{ { Type: ExactIndirectMatch, }, }, }, } // For testing the match-type and upstream ignore rules packageTypeMatches = []Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2", Namespace: "fake-redhat-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kernel-headers", Version: "5.1.0", Type: syftPkg.RpmPkg, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2", Namespace: "fake-debian-vulns", }, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "linux-headers-5.2.0", Version: "5.2.1", Type: syftPkg.DebPkg, }, }, } ) func TestApplyIgnoreRules(t *testing.T) { cases := []struct { name string allMatches []Match ignoreRules []IgnoreRule expectedRemainingMatches []Match expectedIgnoredMatches []IgnoredMatch }{ { name: "no ignore rules", allMatches: allMatches, ignoreRules: nil, expectedRemainingMatches: allMatches, expectedIgnoredMatches: nil, }, { name: "no applicable ignore rules", allMatches: allMatches, ignoreRules: []IgnoreRule{ { Vulnerability: "CVE-789", }, { Package: IgnoreRulePackage{ Name: "bashful", Version: "5", Type: "npm", }, }, { Package: IgnoreRulePackage{ Name: "reach", Version: "3000", }, }, }, expectedRemainingMatches: allMatches, expectedIgnoredMatches: nil, }, { name: "ignore all matches", allMatches: allMatches, ignoreRules: []IgnoreRule{ { Vulnerability: "CVE-123", }, { Package: IgnoreRulePackage{ Location: "/virtual/path/that/has/reach", }, }, }, expectedRemainingMatches: []Match{ allMatches[2], allMatches[3], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: allMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Vulnerability: "CVE-123", }, }, }, { Match: allMatches[1], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Location: "/virtual/path/that/has/reach", }, }, }, }, }, }, { name: "ignore related matches", allMatches: allMatches, ignoreRules: []IgnoreRule{ { Vulnerability: "CVE-123", IncludeAliases: true, }, }, expectedRemainingMatches: []Match{ allMatches[2], allMatches[3], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: allMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Vulnerability: "CVE-123", IncludeAliases: true, }, }, }, { Match: allMatches[1], AppliedIgnoreRules: []IgnoreRule{ { Vulnerability: "CVE-123", IncludeAliases: true, }, }, }, }, }, { name: "ignore subset of matches", allMatches: allMatches, ignoreRules: []IgnoreRule{ { Vulnerability: "CVE-456", }, }, expectedRemainingMatches: []Match{ allMatches[0], allMatches[2], allMatches[3], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: allMatches[1], AppliedIgnoreRules: []IgnoreRule{ { Vulnerability: "CVE-456", }, }, }, }, }, { name: "ignore matches without fix", allMatches: allMatches, ignoreRules: []IgnoreRule{ {FixState: string(vulnerability.FixStateNotFixed)}, {FixState: string(vulnerability.FixStateWontFix)}, {FixState: string(vulnerability.FixStateUnknown)}, }, expectedRemainingMatches: []Match{ allMatches[0], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: allMatches[1], AppliedIgnoreRules: []IgnoreRule{ { FixState: "not-fixed", }, }, }, { Match: allMatches[2], AppliedIgnoreRules: []IgnoreRule{ { FixState: "wont-fix", }, }, }, { Match: allMatches[3], AppliedIgnoreRules: []IgnoreRule{ { FixState: "unknown", }, }, }, }, }, { name: "ignore matches on namespace", allMatches: allMatches, ignoreRules: []IgnoreRule{ {Namespace: "ruby-vulns"}, }, expectedRemainingMatches: []Match{ allMatches[0], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: allMatches[1], AppliedIgnoreRules: []IgnoreRule{ { Namespace: "ruby-vulns", }, }, }, { Match: allMatches[2], AppliedIgnoreRules: []IgnoreRule{ { Namespace: "ruby-vulns", }, }, }, { Match: allMatches[3], AppliedIgnoreRules: []IgnoreRule{ { Namespace: "ruby-vulns", }, }, }, }, }, { name: "ignore matches on language", allMatches: allMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Language: string(syftPkg.Ruby), }, }, }, expectedRemainingMatches: []Match{ allMatches[0], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: allMatches[1], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Language: string(syftPkg.Ruby), }, }, }, }, { Match: allMatches[2], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Language: string(syftPkg.Ruby), }, }, }, }, { Match: allMatches[3], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Language: string(syftPkg.Ruby), }, }, }, }, }, }, { name: "ignore matches on indirect match-type", allMatches: matchTypesMatches, ignoreRules: []IgnoreRule{ { MatchType: ExactIndirectMatch, }, }, expectedRemainingMatches: []Match{ matchTypesMatches[0], matchTypesMatches[2], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: matchTypesMatches[1], AppliedIgnoreRules: []IgnoreRule{ { MatchType: ExactIndirectMatch, }, }, }, }, }, { name: "ignore matches on cpe match-type", allMatches: matchTypesMatches, ignoreRules: []IgnoreRule{ { MatchType: CPEMatch, }, }, expectedRemainingMatches: []Match{ matchTypesMatches[0], matchTypesMatches[1], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: matchTypesMatches[2], AppliedIgnoreRules: []IgnoreRule{ { MatchType: CPEMatch, }, }, }, }, }, { name: "ignore matches on upstream name", allMatches: kernelHeadersMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ UpstreamName: "kernel", }, }, { Package: IgnoreRulePackage{ UpstreamName: "linux-.*", }, }, }, expectedRemainingMatches: []Match{ kernelHeadersMatches[1], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: kernelHeadersMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ UpstreamName: "kernel", }, }, }, }, { Match: kernelHeadersMatches[2], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ UpstreamName: "linux-.*", }, }, }, }, }, }, { name: "ignore matches on package type", allMatches: packageTypeMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Type: string(syftPkg.RpmPkg), }, }, }, expectedRemainingMatches: []Match{ packageTypeMatches[1], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: packageTypeMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Type: string(syftPkg.RpmPkg), }, }, }, }, }, }, { name: "ignore matches rpms for kernel-headers with kernel upstream", allMatches: kernelHeadersMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "kernel-headers", UpstreamName: "kernel", Type: string(syftPkg.RpmPkg), }, MatchType: ExactIndirectMatch, }, { Package: IgnoreRulePackage{ Name: "linux-.*-headers-.*", UpstreamName: "linux.*", Type: string(syftPkg.DebPkg), }, MatchType: ExactIndirectMatch, }, }, expectedRemainingMatches: []Match{ kernelHeadersMatches[1], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: kernelHeadersMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "kernel-headers", UpstreamName: "kernel", Type: string(syftPkg.RpmPkg), }, MatchType: ExactIndirectMatch, }, }, }, { Match: kernelHeadersMatches[2], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "linux-.*-headers-.*", UpstreamName: "linux.*", Type: string(syftPkg.DebPkg), }, MatchType: ExactIndirectMatch, }, }, }, }, }, { name: "ignore on name regex", allMatches: kernelHeadersMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "kernel-headers.*", }, }, }, expectedRemainingMatches: []Match{ kernelHeadersMatches[1], kernelHeadersMatches[2], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: kernelHeadersMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "kernel-headers.*", }, }, }, }, }, }, { name: "ignore on name regex, no matches", allMatches: kernelHeadersMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "foo.*", }, }, }, expectedRemainingMatches: kernelHeadersMatches, expectedIgnoredMatches: nil, }, { name: "ignore on name regex, line termination verification", allMatches: kernelHeadersMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "^kernel-header$", }, }, }, expectedRemainingMatches: kernelHeadersMatches, expectedIgnoredMatches: nil, }, { name: "ignore on name regex, line termination test match", allMatches: kernelHeadersMatches, ignoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "^kernel-headers$", }, }, }, expectedRemainingMatches: []Match{ kernelHeadersMatches[1], kernelHeadersMatches[2], }, expectedIgnoredMatches: []IgnoredMatch{ { Match: kernelHeadersMatches[0], AppliedIgnoreRules: []IgnoreRule{ { Package: IgnoreRulePackage{ Name: "^kernel-headers$", }, }, }, }, }, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { actualRemainingMatches, actualIgnoredMatches := ApplyIgnoreRules(sliceToMatches(testCase.allMatches), testCase.ignoreRules) assertMatchOrder(t, testCase.expectedRemainingMatches, actualRemainingMatches.Sorted()) assertIgnoredMatchOrder(t, testCase.expectedIgnoredMatches, actualIgnoredMatches) }) } } func sliceToMatches(s []Match) Matches { matches := NewMatches() matches.Add(s...) return matches } var ( exampleMatch = Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2000-1234"}, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "a-pkg", Version: "1.0", Locations: file.NewLocationSet( file.NewLocation("/some/path"), file.NewVirtualLocation("/some/path", "/some/virtual/path"), ), Type: "rpm", }, } ) func TestIsRegex(t *testing.T) { tests := []struct { name string input string expected bool }{ // simple strings that should NOT be detected as regex { name: "simple string", input: "hello", expected: false, }, { name: "alphanumeric with dashes", input: "kernel-headers", expected: false, }, { name: "alphanumeric with underscores", input: "my_package_name", expected: false, }, { name: "version numbers", input: "1.2.3", expected: false, // dots are no longer considered regex metacharacters }, { name: "empty string", input: "", expected: false, }, { name: "spaces only", input: " ", expected: false, }, { name: "numbers only", input: "12345", expected: false, }, { name: "letters and numbers", input: "abc123", expected: false, }, { name: "with slashes", input: "path/to/file", expected: false, }, { name: "with colons", input: "namespace:package", expected: false, }, { name: "with at symbol", input: "user@domain.com", expected: false, // dots are no longer considered regex metacharacters }, // strings with regex metacharacters that SHOULD be detected as regex { name: "caret at start", input: "^start", expected: true, }, { name: "dollar at end", input: "end$", expected: true, }, { name: "asterisk wildcard", input: "test*", expected: true, }, { name: "plus quantifier", input: "test+", expected: true, }, { name: "question mark", input: "test?", expected: true, }, { name: "dot wildcard", input: "test.", expected: false, // dots are no longer considered regex metacharacters }, { name: "square brackets", input: "test[abc]", expected: true, }, { name: "parentheses grouping", input: "(test)", expected: true, }, { name: "curly braces quantifier", input: "test{1,3}", expected: true, }, { name: "pipe alternation", input: "test|other", expected: true, }, { name: "backslash escape", input: "test\\", expected: true, }, { name: "multiple metacharacters", input: "^test.*$", expected: true, }, { name: "complex regex pattern", input: "kernel-headers.*", expected: true, }, { name: "anchored regex", input: "^kernel-headers$", expected: true, }, { name: "character class", input: "test[0-9]", expected: true, }, // escaped character classes { name: "escaped digit", input: "\\d", expected: true, }, { name: "escaped non-digit", input: "\\D", expected: true, }, { name: "escaped word character", input: "\\w", expected: true, }, { name: "escaped non-word character", input: "\\W", expected: true, }, { name: "escaped whitespace", input: "\\s", expected: true, }, { name: "escaped non-whitespace", input: "\\S", expected: true, }, { name: "escaped newline", input: "\\n", expected: true, }, { name: "escaped carriage return", input: "\\r", expected: true, }, { name: "escaped tab", input: "\\t", expected: true, }, { name: "escaped form feed", input: "\\f", expected: true, }, { name: "escaped vertical tab", input: "\\v", expected: true, }, { name: "escaped character classes in longer string", input: "prefix\\dpostfix", expected: true, }, { name: "multiple escaped classes", input: "\\w+\\s*\\d+", expected: true, }, // edge cases { name: "single backslash", input: "\\", expected: true, }, { name: "single caret", input: "^", expected: true, }, { name: "single dollar", input: "$", expected: true, }, { name: "single dot", input: ".", expected: false, // dots are no longer considered regex metacharacters }, { name: "backslash followed by regular character", input: "\\a", expected: true, // backslash is still a metacharacter }, { name: "backslash at end", input: "test\\", expected: true, }, { name: "mixed metacharacters and escaped classes", input: "^\\w+\\.\\d{2,}$", expected: true, }, { name: "real world package patterns", input: "linux-.*", expected: true, }, { name: "real world upstream patterns", input: "linux.*", expected: true, }, { name: "real world header patterns", input: "linux-.*-headers-.*", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isLikelyARegex(tt.input) assert.Equal(t, tt.expected, got) }) } } func TestShouldIgnore(t *testing.T) { cases := []struct { name string match Match rule IgnoreRule expected bool }{ { name: "empty rule", match: exampleMatch, rule: IgnoreRule{}, expected: false, }, { name: "rule applies via vulnerability ID", match: exampleMatch, rule: IgnoreRule{ Vulnerability: exampleMatch.Vulnerability.ID, }, expected: true, }, { name: "rule applies via package name", match: exampleMatch, rule: IgnoreRule{ Package: IgnoreRulePackage{ Name: exampleMatch.Package.Name, }, }, expected: true, }, { name: "rule applies via package version", match: exampleMatch, rule: IgnoreRule{ Package: IgnoreRulePackage{ Version: exampleMatch.Package.Version, }, }, expected: true, }, { name: "rule applies via package type", match: exampleMatch, rule: IgnoreRule{ Package: IgnoreRulePackage{ Type: string(exampleMatch.Package.Type), }, }, expected: true, }, { name: "rule applies via package location real path", match: exampleMatch, rule: IgnoreRule{ Package: IgnoreRulePackage{ Location: exampleMatch.Package.Locations.ToSlice()[0].RealPath, }, }, expected: true, }, { name: "rule applies via package location virtual path", match: exampleMatch, rule: IgnoreRule{ Package: IgnoreRulePackage{ Location: exampleMatch.Package.Locations.ToSlice()[1].AccessPath, }, }, expected: true, }, { name: "rule applies via package location glob", match: exampleMatch, rule: IgnoreRule{ Package: IgnoreRulePackage{ Location: "/some/**", }, }, expected: true, }, { name: "rule applies via multiple fields", match: exampleMatch, rule: IgnoreRule{ Vulnerability: exampleMatch.Vulnerability.ID, Package: IgnoreRulePackage{ Type: string(exampleMatch.Package.Type), }, }, expected: true, }, { name: "rule doesn't apply despite some fields matching", match: exampleMatch, rule: IgnoreRule{ Vulnerability: exampleMatch.Vulnerability.ID, Package: IgnoreRulePackage{ Name: "not-the-right-package", Version: exampleMatch.Package.Version, }, }, expected: false, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { actual := len(testCase.rule.IgnoreMatch(testCase.match)) > 0 assert.Equal(t, testCase.expected, actual) }) } } ================================================ FILE: grype/match/match.go ================================================ package match import ( "fmt" "sort" "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" ) var ErrCannotMerge = fmt.Errorf("unable to merge vulnerability matches") // Match represents a finding in the vulnerability matching process, pairing a single package and a single vulnerability object. type Match struct { Vulnerability vulnerability.Vulnerability // The vulnerability details of the match. Package pkg.Package // The package used to search for a match. Details Details // all the ways this particular match was made. } // String is the string representation of select match fields. func (m Match) String() string { return fmt.Sprintf("Match(pkg=%s vuln=%q types=%q)", m.Package, m.Vulnerability.String(), m.Details.Types()) } func (m Match) Fingerprint() Fingerprint { return Fingerprint{ coreFingerprint: coreFingerprint{ vulnerabilityID: m.Vulnerability.ID, vulnerabilityNamespace: m.Vulnerability.Namespace, packageID: m.Package.ID, }, vulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","), } } func (m *Match) Merge(other Match) error { if other.Fingerprint() != m.Fingerprint() { return ErrCannotMerge } // there are cases related vulnerabilities are synthetic, for example when // orienting results by CVE. we need to keep track of these related := strset.New() for _, r := range m.Vulnerability.RelatedVulnerabilities { related.Add(referenceID(r)) } for _, r := range other.Vulnerability.RelatedVulnerabilities { if related.Has(referenceID(r)) { continue } m.Vulnerability.RelatedVulnerabilities = append(m.Vulnerability.RelatedVulnerabilities, r) } // for stable output sort.Slice(m.Vulnerability.RelatedVulnerabilities, func(i, j int) bool { a := m.Vulnerability.RelatedVulnerabilities[i] b := m.Vulnerability.RelatedVulnerabilities[j] return strings.Compare(referenceID(a), referenceID(b)) < 0 }) // also keep details from the other match that are unique detailIDs := strset.New() for _, d := range m.Details { detailIDs.Add(d.ID()) } for _, d := range other.Details { if detailIDs.Has(d.ID()) { continue } m.Details = append(m.Details, d) } // for stable output sort.Sort(m.Details) // retain all unique CPEs for consistent output m.Vulnerability.CPEs = cpe.Merge(m.Vulnerability.CPEs, other.Vulnerability.CPEs) if m.Vulnerability.CPEs == nil { // ensure we always have a non-nil slice m.Vulnerability.CPEs = []cpe.CPE{} } return nil } // referenceID returns an "ID" string for a vulnerability.Reference func referenceID(r vulnerability.Reference) string { return fmt.Sprintf("%s:%s", r.Namespace, r.ID) } ================================================ FILE: grype/match/match_test.go ================================================ package match import ( "testing" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" ) func TestMatch_Merge(t *testing.T) { tests := []struct { name string m1 Match m2 Match expectedErr error expected Match }{ { name: "error on fingerprint mismatch", m1: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace1", }, }, Package: pkg.Package{ ID: "pkg1", }, }, m2: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0002", Namespace: "namespace2", }, }, Package: pkg.Package{ ID: "pkg2", }, }, expectedErr: ErrCannotMerge, }, { name: "merge with unique values", m1: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace", }, RelatedVulnerabilities: []vulnerability.Reference{ { Namespace: "ns1", ID: "ID1", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource), }, }, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ { Type: ExactDirectMatch, SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, m2: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace", }, RelatedVulnerabilities: []vulnerability.Reference{ { Namespace: "ns2", ID: "ID2", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example:1.1:*:*:*:*:*:*:*", cpe.DeclaredSource), }, }, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ { Type: ExactIndirectMatch, SearchedBy: "attr2", Found: "value2", Matcher: "matcher2", }, }, }, expectedErr: nil, expected: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace", }, RelatedVulnerabilities: []vulnerability.Reference{ { Namespace: "ns1", ID: "ID1", }, { Namespace: "ns2", ID: "ID2", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource), cpe.Must("cpe:2.3:a:example:example:1.1:*:*:*:*:*:*:*", cpe.DeclaredSource), }, }, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ { Type: ExactDirectMatch, SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, { Type: ExactIndirectMatch, SearchedBy: "attr2", Found: "value2", Matcher: "matcher2", }, }, }, }, { name: "merges with duplicate values", m1: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace", }, RelatedVulnerabilities: []vulnerability.Reference{ { Namespace: "ns1", ID: "ID1", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource), }, }, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ { Type: ExactDirectMatch, SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, m2: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace", }, RelatedVulnerabilities: []vulnerability.Reference{ { Namespace: "ns1", ID: "ID1", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource), }, }, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ { Type: ExactDirectMatch, SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, expectedErr: nil, expected: Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-0001", Namespace: "namespace", }, RelatedVulnerabilities: []vulnerability.Reference{ { Namespace: "ns1", ID: "ID1", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*", cpe.DeclaredSource), }, }, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ { Type: ExactDirectMatch, SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.m1.Merge(tt.m2) if tt.expectedErr != nil { require.ErrorIs(t, err, tt.expectedErr) } else { require.NoError(t, err) require.Equal(t, tt.expected.Vulnerability.RelatedVulnerabilities, tt.m1.Vulnerability.RelatedVulnerabilities) require.Equal(t, tt.expected.Details, tt.m1.Details) require.Equal(t, tt.expected.Vulnerability.CPEs, tt.m1.Vulnerability.CPEs) } }) } } ================================================ FILE: grype/match/matcher.go ================================================ package match import ( "errors" "fmt" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) // Matcher is the interface to implement to provide top-level package-to-match type Matcher interface { PackageTypes() []syftPkg.Type Type() MatcherType // Match is called for every package found, returning any matches and an optional Ignorer which will be applied // after all matches are found Match(vp vulnerability.Provider, p pkg.Package) ([]Match, []IgnoreFilter, error) } // fatalError can be returned from a Matcher to indicate the matching process should stop. // When fatalError(s) are encountered by the top-level matching process, these will be returned as errors to the caller. type fatalError struct { matcher MatcherType inner error } // NewFatalError creates a new fatalError wrapping the given error func NewFatalError(matcher MatcherType, e error) error { return fatalError{matcher: matcher, inner: e} } // Error implements the error interface for fatalError. func (f fatalError) Error() string { return fmt.Sprintf("%s encountered a fatal error: %v", f.matcher, f.inner) } // IsFatalError returns true if err includes a fatalError func IsFatalError(err error) bool { var fe fatalError return err != nil && errors.As(err, &fe) } ================================================ FILE: grype/match/matcher_type.go ================================================ package match const ( UnknownMatcherType MatcherType = "UnknownMatcherType" StockMatcher MatcherType = "stock-matcher" ApkMatcher MatcherType = "apk-matcher" RubyGemMatcher MatcherType = "ruby-gem-matcher" DpkgMatcher MatcherType = "dpkg-matcher" RpmMatcher MatcherType = "rpm-matcher" JavaMatcher MatcherType = "java-matcher" PythonMatcher MatcherType = "python-matcher" DotnetMatcher MatcherType = "dotnet-matcher" JavascriptMatcher MatcherType = "javascript-matcher" MsrcMatcher MatcherType = "msrc-matcher" PortageMatcher MatcherType = "portage-matcher" GoModuleMatcher MatcherType = "go-module-matcher" OpenVexMatcher MatcherType = "openvex-matcher" CsafVexMatcher MatcherType = "csafvex-matcher" RustMatcher MatcherType = "rust-matcher" BitnamiMatcher MatcherType = "bitnami-matcher" PacmanMatcher MatcherType = "pacman-matcher" HexMatcher MatcherType = "hex-matcher" ) var AllMatcherTypes = []MatcherType{ ApkMatcher, RubyGemMatcher, DpkgMatcher, RpmMatcher, JavaMatcher, PythonMatcher, DotnetMatcher, JavascriptMatcher, MsrcMatcher, PortageMatcher, GoModuleMatcher, OpenVexMatcher, CsafVexMatcher, RustMatcher, BitnamiMatcher, PacmanMatcher, HexMatcher, } type MatcherType string func (t MatcherType) String() string { return string(t) } ================================================ FILE: grype/match/matches.go ================================================ package match import ( "sort" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/log" ) type Matches struct { byFingerprint map[Fingerprint]Match byCoreFingerprint map[coreFingerprint]map[Fingerprint]struct{} byPackage map[pkg.ID]map[Fingerprint]struct{} } func NewMatches(matches ...Match) Matches { m := newMatches() m.Add(matches...) return m } func newMatches() Matches { return Matches{ byFingerprint: make(map[Fingerprint]Match), byCoreFingerprint: make(map[coreFingerprint]map[Fingerprint]struct{}), byPackage: make(map[pkg.ID]map[Fingerprint]struct{}), } } // GetByPkgID returns a slice of potential matches from an ID func (r *Matches) GetByPkgID(id pkg.ID) (matches []Match) { for fingerprint := range r.byPackage[id] { matches = append(matches, r.byFingerprint[fingerprint]) } return matches } // AllByPkgID returns a map of all matches organized by package ID func (r *Matches) AllByPkgID() map[pkg.ID][]Match { matches := make(map[pkg.ID][]Match) for id, fingerprints := range r.byPackage { for fingerprint := range fingerprints { matches[id] = append(matches[id], r.byFingerprint[fingerprint]) } } return matches } func (r *Matches) Merge(other Matches) { for _, fingerprints := range other.byPackage { for fingerprint := range fingerprints { r.Add(other.byFingerprint[fingerprint]) } } } func (r *Matches) Diff(other Matches) *Matches { diff := newMatches() for fingerprint := range r.byFingerprint { if _, exists := other.byFingerprint[fingerprint]; !exists { diff.Add(r.byFingerprint[fingerprint]) } } return &diff } func (r *Matches) Add(matches ...Match) { for _, newMatch := range matches { newFp := newMatch.Fingerprint() // add or merge the new match with an existing match r.addOrMerge(newMatch, newFp) // track common elements (core fingerprint + package index) if _, exists := r.byCoreFingerprint[newFp.coreFingerprint]; !exists { r.byCoreFingerprint[newFp.coreFingerprint] = make(map[Fingerprint]struct{}) } r.byCoreFingerprint[newFp.coreFingerprint][newFp] = struct{}{} if _, exists := r.byPackage[newMatch.Package.ID]; !exists { r.byPackage[newMatch.Package.ID] = make(map[Fingerprint]struct{}) } r.byPackage[newMatch.Package.ID][newFp] = struct{}{} } } func (r *Matches) addOrMerge(newMatch Match, newFp Fingerprint) { // a) if there is an exact fingerprint match, then merge with that // b) otherwise, look for core fingerprint matches (looser rules) // we prefer direct matches to indirect matches: // 1. if the new match is a direct match and there is an indirect match, replace the indirect match with the direct match // 2. if the new match is an indirect match and there is a direct match, merge with the existing direct match // c) this is a new match if existingMatch, exists := r.byFingerprint[newFp]; exists { // case A if err := existingMatch.Merge(newMatch); err != nil { log.WithFields("original", existingMatch.String(), "new", newMatch.String(), "error", err).Warn("unable to merge matches") // at least capture the additional details existingMatch.Details = append(existingMatch.Details, newMatch.Details...) } r.byFingerprint[newFp] = existingMatch } else if existingFingerprints, exists := r.byCoreFingerprint[newFp.coreFingerprint]; exists { // case B if !r.mergeCoreMatches(newMatch, newFp, existingFingerprints) { // case C (we should not drop this match if we were unable to merge it) r.byFingerprint[newFp] = newMatch } } else { // case C r.byFingerprint[newFp] = newMatch } } func (r *Matches) mergeCoreMatches(newMatch Match, newFp Fingerprint, existingFingerprints map[Fingerprint]struct{}) bool { for existingFp := range existingFingerprints { existingMatch := r.byFingerprint[existingFp] shouldSupersede := hasMatchType(newMatch.Details, ExactDirectMatch) && hasExclusivelyAnyMatchTypes(existingMatch.Details, ExactIndirectMatch) if shouldSupersede { // case B1 if replaced := r.replace(newMatch, existingFp, newFp, existingMatch.Details...); !replaced { log.WithFields("original", existingMatch.String(), "new", newMatch.String()).Trace("unable to replace match") // at least capture the new details existingMatch.Details = append(existingMatch.Details, newMatch.Details...) } else { return true } } // case B2 if err := existingMatch.Merge(newMatch); err != nil { log.WithFields("original", existingMatch.String(), "new", newMatch.String(), "error", err).Trace("unable to merge matches") // at least capture the new details existingMatch.Details = append(existingMatch.Details, newMatch.Details...) } else { return true } } return false } func (r *Matches) replace(m Match, ogFp, newFp Fingerprint, extraDetails ...Detail) bool { if ogFp.coreFingerprint != newFp.coreFingerprint { return false } // update indexes for pkgID, fingerprints := range r.byPackage { if _, exists := fingerprints[ogFp]; exists { delete(fingerprints, ogFp) fingerprints[newFp] = struct{}{} r.byPackage[pkgID] = fingerprints } } // update the match delete(r.byFingerprint, ogFp) m.Details = append(m.Details, extraDetails...) sort.Sort(m.Details) r.byFingerprint[newFp] = m return true } func (r *Matches) Enumerate() <-chan Match { channel := make(chan Match) go func() { defer close(channel) for _, match := range r.byFingerprint { channel <- match } }() return channel } func (r *Matches) Sorted() []Match { matches := make([]Match, 0) for m := range r.Enumerate() { matches = append(matches, m) } sort.Sort(ByElements(matches)) return matches } // Count returns the total number of matches in a result func (r *Matches) Count() int { return len(r.byFingerprint) } func hasMatchType(details Details, ty Type) bool { for _, d := range details { if d.Type == ty { return true } } return false } func hasExclusivelyAnyMatchTypes(details Details, tys ...Type) bool { allowed := strset.New() for _, ty := range tys { allowed.Add(string(ty)) } var found bool for _, d := range details { if allowed.Has(string(d.Type)) { found = true } else { return false } } return found } ================================================ FILE: grype/match/matches_test.go ================================================ package match import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatchesSortMixedDimensions(t *testing.T) { first := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } second := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-a", Version: "1.0.0", Type: syftPkg.NpmPkg, }, } third := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-a", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } fourth := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-c", Version: "3.0.0", Type: syftPkg.ApkPkg, }, } fifth := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-d", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } sixth := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, Fix: vulnerability.Fix{ Versions: []string{"2.0.0", "1.0.0"}, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-d", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } seventh := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, Fix: vulnerability.Fix{ Versions: []string{"2.0.1"}, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-d", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } eighth := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, Fix: vulnerability.Fix{ Versions: []string{"3.0.0"}, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-d", Version: "2.0.0", Type: syftPkg.RpmPkg, Locations: file.NewLocationSet(file.NewLocation("/some/first-path")), }, } ninth := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, Fix: vulnerability.Fix{ Versions: []string{"3.0.0"}, }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-d", Version: "2.0.0", Type: syftPkg.RpmPkg, Locations: file.NewLocationSet(file.NewLocation("/some/other-path")), }, } input := []Match{ // shuffle vulnerability id, package name, package version, and package type ninth, fifth, eighth, third, seventh, first, sixth, second, fourth, } matches := NewMatches(input...) assertMatchOrder(t, []Match{first, second, third, fourth, fifth, sixth, seventh, eighth, ninth}, matches.Sorted()) } func TestMatchesSortByVulnerability(t *testing.T) { first := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } second := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0020", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } input := []Match{ second, first, } matches := NewMatches(input...) assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } func TestMatches_AllByPkgID(t *testing.T) { first := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } second := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-c", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } input := []Match{ second, first, } matches := NewMatches(input...) expected := map[pkg.ID][]Match{ first.Package.ID: { first, }, second.Package.ID: { second, }, } assert.Equal(t, expected, matches.AllByPkgID()) } func TestMatchesSortByPackage(t *testing.T) { first := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } second := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-c", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } input := []Match{ second, first, } matches := NewMatches(input...) assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } func TestMatchesSortByPackageVersion(t *testing.T) { first := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } second := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "2.0.0", Type: syftPkg.RpmPkg, }, } input := []Match{ second, first, } matches := NewMatches(input...) assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } func TestMatchesSortByPackageType(t *testing.T) { first := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.ApkPkg, }, } second := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-0010", }, }, Package: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package-b", Version: "1.0.0", Type: syftPkg.RpmPkg, }, } input := []Match{ second, first, } matches := NewMatches(input...) assertMatchOrder(t, []Match{first, second}, matches.Sorted()) } func assertMatchOrder(t *testing.T, expected, actual []Match) { var expectedStr []string for _, e := range expected { expectedStr = append(expectedStr, e.Package.Name) } var actualStr []string for _, a := range actual { actualStr = append(actualStr, a.Package.Name) } // makes this easier on the eyes to sanity check... require.Equal(t, expectedStr, actualStr) // make certain the fields are what you'd expect assert.Equal(t, expected, actual) } func assertIgnoredMatchOrder(t *testing.T, expected, actual []IgnoredMatch) { var expectedStr []string for _, e := range expected { expectedStr = append(expectedStr, e.Package.Name) } var actualStr []string for _, a := range actual { actualStr = append(actualStr, a.Package.Name) } // makes this easier on the eyes to sanity check... require.Equal(t, expectedStr, actualStr) // make certain the fields are what you'd expect assert.Equal(t, expected, actual) } func TestMatches_Diff(t *testing.T) { a := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "vuln-a", Namespace: "name-a", }, }, Package: pkg.Package{ ID: "package-a", }, } b := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "vuln-b", Namespace: "name-b", }, }, Package: pkg.Package{ ID: "package-b", }, } c := Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "vuln-c", Namespace: "name-c", }, }, Package: pkg.Package{ ID: "package-c", }, } tests := []struct { name string subject Matches other Matches want Matches }{ { name: "no diff", subject: NewMatches(a, b, c), other: NewMatches(a, b, c), want: newMatches(), }, { name: "extra items in subject", subject: NewMatches(a, b, c), other: NewMatches(a, b), want: NewMatches(c), }, { // this demonstrates that this is not meant to implement a symmetric diff name: "extra items in other (results in no diff)", subject: NewMatches(a, b), other: NewMatches(a, b, c), want: NewMatches(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, &tt.want, tt.subject.Diff(tt.other), "Diff(%v)", tt.other) }) } } func TestMatches_Add_Merge(t *testing.T) { commonVuln := "CVE-2023-0001" commonNamespace := "namespace1" commonVulnerability := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: commonVuln, Namespace: commonNamespace, }, Constraint: func() version.Constraint { c, err := version.GetConstraint("< 1.0.0", version.SemanticFormat) require.NoError(t, err) return c }(), Fix: vulnerability.Fix{ Versions: []string{"1.0.0"}, }, } commonDirectDetail := Detail{ Type: ExactDirectMatch, SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", } matchPkg1Direct := Match{ Vulnerability: commonVulnerability, Package: pkg.Package{ ID: "pkg1", }, Details: Details{ commonDirectDetail, }, } matchPkg2Indirect := Match{ Vulnerability: commonVulnerability, Package: pkg.Package{ ID: "pkg2", }, Details: Details{ { Type: ExactIndirectMatch, SearchedBy: "attr2", Found: "value2", Matcher: "matcher2", }, }, } tests := []struct { name string matches []Match expectedMatches map[string][]Match }{ { name: "adds new match without merging", matches: []Match{matchPkg1Direct, matchPkg2Indirect}, expectedMatches: map[string][]Match{ "pkg1": { matchPkg1Direct, }, "pkg2": { matchPkg2Indirect, }, }, }, { name: "merges matches with identical fingerprints", matches: []Match{ matchPkg1Direct, { Vulnerability: matchPkg1Direct.Vulnerability, Package: matchPkg1Direct.Package, Details: Details{ { Type: ExactIndirectMatch, // different! SearchedBy: "attr2", // different! Found: "value2", // different! Matcher: "matcher2", // different! }, }, }, }, expectedMatches: map[string][]Match{ "pkg1": { { Vulnerability: commonVulnerability, Package: matchPkg1Direct.Package, Details: Details{ commonDirectDetail, { Type: ExactIndirectMatch, SearchedBy: "attr2", Found: "value2", Matcher: "matcher2", }, }, }, }, }, }, { name: "merges matches with different fingerprints but semantically the same", matches: []Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: commonVuln, Namespace: commonNamespace, }, Constraint: func() version.Constraint { // different! c, err := version.GetConstraint("< 3.2.12", version.SemanticFormat) require.NoError(t, err) return c }(), Fix: vulnerability.Fix{ Versions: []string{"3.2.12"}, // different! }, }, Package: matchPkg1Direct.Package, Details: Details{ { Type: ExactIndirectMatch, // different! SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, matchPkg1Direct, }, expectedMatches: map[string][]Match{ "pkg1": { { Vulnerability: commonVulnerability, Package: matchPkg1Direct.Package, Details: Details{ commonDirectDetail, // sorts to first (direct should be prioritized over indirect) { Type: ExactIndirectMatch, // different! SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, }, }, }, { name: "does not merge matches with different fingerprints but semantically the same when matched by CPE", matches: []Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: commonVuln, Namespace: commonNamespace, }, Constraint: func() version.Constraint { // different! c, err := version.GetConstraint("< 3.2.12", version.SemanticFormat) require.NoError(t, err) return c }(), Fix: vulnerability.Fix{ Versions: []string{"3.2.12"}, // different! }, }, Package: matchPkg1Direct.Package, Details: Details{ { Type: CPEMatch, // different! SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, matchPkg1Direct, }, expectedMatches: map[string][]Match{ "pkg1": { { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: commonVuln, Namespace: commonNamespace, }, Constraint: func() version.Constraint { // different! c, err := version.GetConstraint("< 3.2.12", version.SemanticFormat) require.NoError(t, err) return c }(), Fix: vulnerability.Fix{ Versions: []string{"3.2.12"}, // different! }, }, Package: matchPkg1Direct.Package, Details: Details{ { Type: CPEMatch, // different! SearchedBy: "attr1", Found: "value1", Matcher: "matcher1", }, }, }, matchPkg1Direct, }, }, }, } cmpOpts := []cmp.Option{ cmpopts.IgnoreUnexported(vulnerability.Vulnerability{}, pkg.Package{}, file.Location{}, file.LocationSet{}), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.EquateEmpty(), cmpopts.SortSlices(func(a, b Match) bool { return ByElements([]Match{a, b}).Less(0, 1) }), } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := NewMatches(tt.matches...) require.NotEmpty(t, tt.expectedMatches) for pkgId, expected := range tt.expectedMatches { storedMatches := actual.GetByPkgID(pkg.ID(pkgId)) if d := cmp.Diff(expected, storedMatches, cmpOpts...); d != "" { t.Errorf("unexpected matches for %q (-want, +got): %s", pkgId, d) } } assert.Len(t, actual.byPackage, len(tt.expectedMatches)) }) } } ================================================ FILE: grype/match/provider.go ================================================ package match type ExclusionProvider interface { IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error) } ================================================ FILE: grype/match/results.go ================================================ package match import ( "fmt" "sort" "github.com/scylladb/go-set/strset" ) type CPEParameters struct { Namespace string `json:"namespace"` CPEs []string `json:"cpes"` Package PackageParameter `json:"package"` } type PackageParameter struct { Name string `json:"name"` Version string `json:"version"` } func (i *CPEParameters) Merge(other CPEParameters) error { if i.Namespace != other.Namespace { return fmt.Errorf("namespaces do not match") } existingCPEs := strset.New(i.CPEs...) newCPEs := strset.New(other.CPEs...) mergedCPEs := strset.Union(existingCPEs, newCPEs).List() sort.Strings(mergedCPEs) i.CPEs = mergedCPEs return nil } type CPEResult struct { VulnerabilityID string `json:"vulnerabilityID"` VersionConstraint string `json:"versionConstraint"` CPEs []string `json:"cpes"` } func (h CPEResult) Equals(other CPEResult) bool { if h.VersionConstraint != other.VersionConstraint { return false } if len(h.CPEs) != len(other.CPEs) { return false } for i := range h.CPEs { if h.CPEs[i] != other.CPEs[i] { return false } } return true } type DistroParameters struct { Distro DistroIdentification `json:"distro"` Package PackageParameter `json:"package"` Namespace string `json:"namespace"` } type DistroIdentification struct { Type string `json:"type"` Version string `json:"version"` } func (d *DistroParameters) Merge(other DistroParameters) error { if d.Namespace != other.Namespace { return fmt.Errorf("namespaces do not match") } if d.Distro.Type != other.Distro.Type { return fmt.Errorf("distro types do not match") } if d.Distro.Version != other.Distro.Version { return fmt.Errorf("distro versions do not match") } if d.Package.Name != other.Package.Name { return fmt.Errorf("package names do not match") } if d.Package.Version != other.Package.Version { return fmt.Errorf("package versions do not match") } return nil } type DistroResult struct { VulnerabilityID string `json:"vulnerabilityID"` VersionConstraint string `json:"versionConstraint"` } func (d DistroResult) Equals(other DistroResult) bool { return d.VulnerabilityID == other.VulnerabilityID && d.VersionConstraint == other.VersionConstraint } type EcosystemParameters struct { Language string `json:"language"` Namespace string `json:"namespace"` Package PackageParameter `json:"package"` } func (e *EcosystemParameters) Merge(other EcosystemParameters) error { if e.Namespace != other.Namespace { return fmt.Errorf("namespaces do not match") } if e.Language != other.Language { return fmt.Errorf("languages do not match") } if e.Package.Name != other.Package.Name { return fmt.Errorf("package names do not match") } if e.Package.Version != other.Package.Version { return fmt.Errorf("package versions do not match") } return nil } type EcosystemResult struct { VulnerabilityID string `json:"vulnerabilityID"` VersionConstraint string `json:"versionConstraint"` } ================================================ FILE: grype/match/sort.go ================================================ package match import ( "sort" "strings" ) var _ sort.Interface = (*ByElements)(nil) type ByElements []Match // Len is the number of elements in the collection. func (m ByElements) Len() int { return len(m) } // Less reports whether the element with index i should sort before the element with index j. func (m ByElements) Less(i, j int) bool { if m[i].Vulnerability.ID == m[j].Vulnerability.ID { if m[i].Package.Name == m[j].Package.Name { if m[i].Package.Version == m[j].Package.Version { if m[i].Package.Type == m[j].Package.Type { // this is an approximate ordering, but is not accurate in terms of semver and other version formats // but stability is what is important here, not the accuracy of the sort. fixVersions1 := m[i].Vulnerability.Fix.Versions fixVersions2 := m[j].Vulnerability.Fix.Versions sort.Strings(fixVersions1) sort.Strings(fixVersions2) fixStr1 := strings.Join(fixVersions1, ",") fixStr2 := strings.Join(fixVersions2, ",") if fixStr1 == fixStr2 { loc1 := m[i].Package.Locations.ToSlice() loc2 := m[j].Package.Locations.ToSlice() var locStr1 string for _, location := range loc1 { locStr1 += location.RealPath } var locStr2 string for _, location := range loc2 { locStr2 += location.RealPath } return locStr1 < locStr2 } return fixStr1 < fixStr2 } return m[i].Package.Type < m[j].Package.Type } return m[i].Package.Version < m[j].Package.Version } return m[i].Package.Name < m[j].Package.Name } return m[i].Vulnerability.ID < m[j].Vulnerability.ID } // Swap swaps the elements with indexes i and j. func (m ByElements) Swap(i, j int) { m[i], m[j] = m[j], m[i] } ================================================ FILE: grype/match/type.go ================================================ package match import ( "github.com/anchore/grype/grype/pkg" ) const ( ExactDirectMatch Type = "exact-direct-match" ExactIndirectMatch Type = "exact-indirect-match" CPEMatch Type = "cpe-match" ) var typeOrder = map[Type]int{ ExactDirectMatch: 1, ExactIndirectMatch: 2, CPEMatch: 3, } type Type string func (t Type) String() string { return string(t) } func ConvertToIndirectMatches(matches []Match, p pkg.Package) { for idx := range matches { for dIdx := range matches[idx].Details { // only override the match details to "indirect" if the match details are explicitly indicate a "direct" match if matches[idx].Details[dIdx].Type == ExactDirectMatch { matches[idx].Details[dIdx].Type = ExactIndirectMatch } } // we always override the package to the direct package matches[idx].Package = p } } ================================================ FILE: grype/matcher/apk/matcher.go ================================================ package apk import ( "errors" "fmt" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" syftPkg "github.com/anchore/syft/syft/pkg" ) var ( nakVersionString = version.MustGetConstraint("< 0", version.ApkFormat).String() // nakConstraint checks the exact version string for being an APK version with "< 0" nakConstraint = search.ByConstraintFunc(func(c version.Constraint) (bool, error) { return c.String() == nakVersionString, nil }) ) type Matcher struct{} func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.ApkPkg} } func (m *Matcher) Type() match.MatcherType { return match.ApkMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match var ignoreFilters []match.IgnoreFilter // direct matches with package itself (+ distro-fixed ignore rules when metadata implements FileOwner) directMatches, directIgnores, err := m.findMatchesForPackage(store, p, nil) if err != nil { return nil, nil, err } matches = append(matches, directMatches...) ignoreFilters = append(ignoreFilters, directIgnores...) // indirect matches, via package's origin package indirectMatches, indirectIgnores, err := m.findMatchesForOriginPackage(store, p) if err != nil { return nil, nil, err } matches = append(matches, indirectMatches...) ignoreFilters = append(ignoreFilters, indirectIgnores...) // APK sources are also able to NAK vulnerabilities, so we want to return these as explicit ignores in order // to allow rules later to use these to ignore "the same" vulnerability found in "the same" locations naks, err := m.findNaksForPackage(store, p) if err != nil { return nil, nil, err } ignoreFilters = append(ignoreFilters, naks...) return matches, ignoreFilters, nil } //nolint:funlen,gocognit func (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p pkg.Package) ([]match.Match, error) { // find CPE-indexed vulnerability matches specific to the given package name and version cpeMatches, err := internal.MatchPackageByCPEs(provider, p, m.Type()) if err != nil { log.WithFields("package", p.Name, "error", err).Debug("failed to find CPE matches for package") } if p.Distro == nil { return cpeMatches, nil } cpeMatchesByID := matchesByID(cpeMatches) // remove cpe matches where there is an entry in the secDB for the particular package-vulnerability pairing, and the // installed package version is >= the fixed in version for the secDB record. secDBVulnerabilities, err := provider.FindVulnerabilities( search.ByPackageName(p.Name), search.ByDistro(*p.Distro)) if err != nil { return nil, err } for _, upstreamPkg := range pkg.UpstreamPackages(p) { secDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities( search.ByPackageName(upstreamPkg.Name), search.ByDistro(*upstreamPkg.Distro)) if err != nil { return nil, err } secDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...) } secDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities) verObj := version.New(p.Version, pkg.VersionFormat(p)) var finalCpeMatches []match.Match cveLoop: for id, cpeMatchesForID := range cpeMatchesByID { // check to see if there is a secdb entry for this ID (CVE) secDBVulnerabilitiesForID, exists := secDBVulnerabilitiesByID[id] if !exists { // does not exist in secdb, so the CPE record(s) should be added to the final results // remove fixed-in versions, since NVD doesn't know when Alpine will fix things for _, nvdOnlyMatch := range cpeMatchesForID { if len(nvdOnlyMatch.Vulnerability.Fix.Versions) > 0 { nvdOnlyMatch.Vulnerability.Fix = vulnerability.Fix{ State: vulnerability.FixStateUnknown, } } finalCpeMatches = append(finalCpeMatches, nvdOnlyMatch) } continue } // there is a secdb entry... for _, vuln := range secDBVulnerabilitiesForID { // ...is there a fixed in entry? (should always be yes) if len(vuln.Fix.Versions) == 0 { continue } // ...is the current package vulnerable? vulnerable, err := vuln.Constraint.Satisfied(verObj) if err != nil { return nil, err } if vulnerable { // if there is at least one vulnerable entry, then all CPE record(s) should be added to the final results finalCpeMatches = append(finalCpeMatches, cpeMatchesForID...) continue cveLoop } } } return finalCpeMatches, nil } func deduplicateMatches(secDBMatches, cpeMatches []match.Match) (matches []match.Match) { // add additional unique matches from CPE source that is unique from the SecDB matches secDBMatchesByID := matchesByID(secDBMatches) cpeMatchesByID := matchesByID(cpeMatches) for id, cpeMatchesForID := range cpeMatchesByID { // by this point all matches have been verified to be vulnerable within the given package version relative to the vulnerability source. // now we will add unique CPE candidates that were not found in secdb. if _, exists := secDBMatchesByID[id]; !exists { // add the new CPE-based record (e.g. NVD) since it was not found in secDB matches = append(matches, cpeMatchesForID...) } } return matches } func matchesByID(matches []match.Match) map[string][]match.Match { var results = make(map[string][]match.Match) for _, secDBMatch := range matches { results[secDBMatch.Vulnerability.ID] = append(results[secDBMatch.Vulnerability.ID], secDBMatch) } return results } func vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulnerability.Vulnerability { var results = make(map[string][]vulnerability.Vulnerability) for _, vuln := range vulns { results[vuln.ID] = append(results[vuln.ID], vuln) } return results } func (m *Matcher) findMatchesForPackage(store vulnerability.Provider, p pkg.Package, catalogPkg *pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { // find SecDB matches for the given package name and version // APK doesn't use epochs, so pass nil for the config secDBMatches, secDBIgnores, err := internal.MatchPackageByDistroWithOwnedFiles(store, p, catalogPkg, m.Type(), nil) if err != nil { return nil, nil, err } // TODO: are there other errors that we should handle here that causes this to short circuit cpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, p) if err != nil && !errors.Is(err, internal.ErrEmptyCPEMatch) { return nil, nil, err } var matches []match.Match // keep all secdb matches, as this is an authoritative source matches = append(matches, secDBMatches...) // keep only unique CPE matches matches = append(matches, deduplicateMatches(secDBMatches, cpeMatches)...) return matches, secDBIgnores, nil } func (m *Matcher) findMatchesForOriginPackage(store vulnerability.Provider, catalogPkg pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match var ignores []match.IgnoreFilter for _, indirectPackage := range pkg.UpstreamPackages(catalogPkg) { indirectMatches, indirectIgnores, err := m.findMatchesForPackage(store, indirectPackage, &catalogPkg) if err != nil { return nil, nil, fmt.Errorf("failed to find vulnerabilities for apk upstream source package: %w", err) } matches = append(matches, indirectMatches...) ignores = append(ignores, indirectIgnores...) } // we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package) // however, we also want to keep the indirect package around for future reference match.ConvertToIndirectMatches(matches, catalogPkg) return matches, ignores, nil } // ownedFilesFromMetadata returns the files owned by a package if its metadata implements pkg.FileOwner. func ownedFilesFromMetadata(p pkg.Package) []string { if fo, ok := p.Metadata.(pkg.FileOwner); ok { return fo.OwnedFiles() } return nil } // NAK entries are those reported as explicitly not vulnerable by the upstream provider, // for example this entry is present in the v5 database: // 312891,CVE-2020-7224,openvpn,alpine:distro:alpine:3.10,,< 0,apk,,"[{""id"":""CVE-2020-7224"",""namespace"":""nvd:cpe""}]","[""0""]",fixed, // which indicates, for the alpine:3.10 distro, package openvpn is not vulnerable to CVE-2020-7224 // we want to report these NAK entries as match.IgnoredMatch, to allow for later processing to create ignore rules // based on packages which overlap by location, such as a python binary found in addition to the python APK entry -- // we want to NAK this vulnerability for BOTH packages func (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Package) ([]match.IgnoreFilter, error) { if p.Distro == nil { return nil, nil } // get all the direct naks naks, err := provider.FindVulnerabilities( search.ByDistro(*p.Distro), search.ByPackageName(p.Name), nakConstraint, ) if err != nil { return nil, err } // append all the upstream naks for _, upstreamPkg := range pkg.UpstreamPackages(p) { upstreamNaks, err := provider.FindVulnerabilities( search.ByDistro(*upstreamPkg.Distro), search.ByPackageName(upstreamPkg.Name), nakConstraint, ) if err != nil { return nil, err } naks = append(naks, upstreamNaks...) } paths := ownedFilesFromMetadata(p) if len(paths) == 0 { return nil, nil } var ignores []match.IgnoreFilter for _, nak := range naks { for _, path := range paths { ignores = append(ignores, match.IgnoreRule{ Vulnerability: nak.ID, IncludeAliases: true, Reason: "Explicit APK NAK", Package: match.IgnoreRulePackage{ Location: path, }, }) } } return ignores, nil } ================================================ FILE: grype/matcher/apk/matcher_test.go ================================================ package apk import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestSecDBOnlyMatch(t *testing.T) { secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ // ID doesn't match - this is the key for comparison in the matcher ID: "CVE-2020-2", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat), } vp := mock.VulnerabilityProvider(secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } expected := []match.Match{ { Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, Namespace: "secdb:distro:alpine:3.12", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-2", VersionConstraint: secDbVuln.Constraint.String(), }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestBothSecdbAndNvdMatches(t *testing.T) { // NVD and Alpine's secDB both have the same CVE ID for the package nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ // ID *does* match - this is the key for comparison in the matcher ID: "CVE-2020-1", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat), } vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } expected := []match.Match{ { // ensure the SECDB record is preferred over the NVD record Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, Namespace: "secdb:distro:alpine:3.12", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-1", VersionConstraint: secDbVuln.Constraint.String(), }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) { // NVD and Alpine's secDB both have the same CVE ID for the package nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 1.0.0", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, Fix: vulnerability.Fix{ Versions: []string{"1.0.0"}, State: vulnerability.FixStateFixed, }, } secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ // ID *does* match - this is the key for comparison in the matcher ID: "CVE-2020-1", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.12", version.ApkFormat), // SecDB indicates Alpine have backported a fix to v0.9... Fix: vulnerability.Fix{ Versions: []string{"0.9.12"}, State: vulnerability.FixStateFixed, }, } vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } expected := []match.Match{ { // ensure the SECDB record is preferred over the NVD record Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, Namespace: "secdb:distro:alpine:3.12", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-1", VersionConstraint: secDbVuln.Constraint.String(), }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) { // NVD and Alpine's secDB both have the same CVE ID for the package nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), // Note: the product name is NOT the same as the target package name CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:lib_vnc_project-(server):libvncumbrellaproject:*:*:*:*:*:*:*:*", ""), }, } secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ // ID *does* match - this is the key for comparison in the matcher ID: "CVE-2020-1", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.ApkFormat), } vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ // Note: the product name is NOT the same as the package name cpe.Must("cpe:2.3:a:*:libvncumbrellaproject:0.9.9:*:*:*:*:*:*:*", ""), }, } expected := []match.Match{ { // ensure the SECDB record is preferred over the NVD record Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, Namespace: "secdb:distro:alpine:3.12", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-1", VersionConstraint: secDbVuln.Constraint.String(), }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdOnlyMatches(t *testing.T) { nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):lib/vncserver:*:*:*:*:*:*:*:*`, ""), }, } vp := mock.VulnerabilityProvider(nvdVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:lib/vncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } expected := []match.Match{ { Vulnerability: nvdVuln, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:lib\\/vncserver:0.9.9:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, Found: match.CPEResult{ // use .String() for proper escaping CPEs: []string{nvdVuln.CPEs[0].Attributes.String()}, VersionConstraint: nvdVuln.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdOnlyMatches_FixInNvd(t *testing.T) { nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.11", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, Fix: vulnerability.Fix{ Versions: []string{"0.9.12"}, State: vulnerability.FixStateFixed, }, } vp := mock.VulnerabilityProvider(nvdVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.9", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } vulnFound := nvdVuln // Important: for alpine matcher, fix version can come from secDB but _not_ from // NVD data. vulnFound.Fix = vulnerability.Fix{State: vulnerability.FixStateUnknown} expected := []match.Match{ { Vulnerability: vulnFound, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, Found: match.CPEResult{ CPEs: []string{vulnFound.CPEs[0].Attributes.String()}, VersionConstraint: vulnFound.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdMatchesProperVersionFiltering(t *testing.T) { nvdVulnMatch := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("<= 0.9.11", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } nvdVulnNoMatch := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-2", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.11", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } vp := mock.VulnerabilityProvider(nvdVulnMatch, nvdVulnNoMatch) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.11-r10", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*", ""), }, } expected := []match.Match{ { Vulnerability: nvdVulnMatch, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.11-r10", }, }, Found: match.CPEResult{ CPEs: []string{nvdVulnMatch.CPEs[0].Attributes.String()}, VersionConstraint: nvdVulnMatch.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdMatchesWithSecDBFix(t *testing.T) { nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("> 0.9.0, < 0.10.0", version.UnknownFormat), // note: this is not normal NVD configuration, but has the desired effect of a "wide net" for vulnerable indication CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.11", version.ApkFormat), // note: this does NOT include 0.9.11, so NVD and SecDB mismatch here... secDB should trump in this case } vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.11", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } var expected []match.Match actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) { nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("", version.UnknownFormat), // note: empty value indicates that all versions are vulnerable CPEs: []cpe.CPE{ cpe.Must(`cpe:2.3:a:lib_vnc_project-\(server\):libvncserver:*:*:*:*:*:*:*:*`, ""), }, } secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.11", version.ApkFormat), } vp := mock.VulnerabilityProvider(nvdVuln, secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "libvncserver", Version: "0.9.11", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*", ""), }, } var expected []match.Match actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) { nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2015-3211", Namespace: "nvd:cpe", }, PackageName: "php-fpm", Constraint: version.MustGetConstraint("", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:php-fpm:php-fpm:-:*:*:*:*:*:*:*", ""), }, } secDBVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2015-3211", Namespace: "wolfi:distro:wolfi:rolling", }, PackageName: "php-8.3", Constraint: version.MustGetConstraint("< 0", version.ApkFormat), } vp := mock.VulnerabilityProvider(nvdVuln, secDBVuln) m := Matcher{} d := distro.New(distro.Wolfi, "", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "php-8.3-fpm", // the package will not match anything Version: "8.3.11-r0", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:php-fpm:php-fpm:8.3.11-r0:*:*:*:*:*:*:*", ""), }, Upstreams: []pkg.UpstreamPackage{ { Name: "php-8.3", // this upstream should match Version: "8.3.11-r0", }, }, } var expected []match.Match actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestDistroMatchBySourceIndirection(t *testing.T) { secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ // ID doesn't match - this is the key for comparison in the matcher ID: "CVE-2020-2", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "musl", Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.ApkFormat), } vp := mock.VulnerabilityProvider(secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, Distro: d, Upstreams: []pkg.UpstreamPackage{ { Name: "musl", }, }, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", cpe.GeneratedSource), }, } expected := []match.Match{ { Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { Type: match.ExactIndirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "musl", Version: p.Version, }, Namespace: "secdb:distro:alpine:3.12", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-2", VersionConstraint: secDbVuln.Constraint.String(), }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) { // this should match the test package // the test package will have no CPE causing an error, // but the error should not cause the secDB matches to fail secDbVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-2", Namespace: "secdb:distro:alpine:3.12", }, PackageName: "musl", Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.ApkFormat), } vp := mock.VulnerabilityProvider(secDbVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, Distro: d, Upstreams: []pkg.UpstreamPackage{ { Name: "musl", }, }, CPEs: []cpe.CPE{}, } expected := []match.Match{ { Vulnerability: secDbVuln, Package: p, Details: []match.Detail{ { Type: match.ExactIndirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "musl", Version: p.Version, }, Namespace: "secdb:distro:alpine:3.12", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-2", VersionConstraint: secDbVuln.Constraint.String(), }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func TestNVDMatchBySourceIndirection(t *testing.T) { nvdVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-1", Namespace: "nvd:cpe", }, PackageName: "musl", Constraint: version.MustGetConstraint("<= 1.3.3-r0", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*", ""), }, } vp := mock.VulnerabilityProvider(nvdVuln) m := Matcher{} d := distro.New(distro.Alpine, "3.12.0", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "musl-utils", Version: "1.3.2-r0", Type: syftPkg.ApkPkg, Distro: d, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*", ""), }, Upstreams: []pkg.UpstreamPackage{ { Name: "musl", }, }, } expected := []match.Match{ { Vulnerability: nvdVuln, Package: p, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:musl:musl:1.3.2-r0:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "musl", Version: "1.3.2-r0", }, }, Found: match.CPEResult{ CPEs: []string{nvdVuln.CPEs[0].Attributes.String()}, VersionConstraint: nvdVuln.Constraint.String(), VulnerabilityID: "CVE-2020-1", }, Matcher: match.ApkMatcher, }, }, }, } actual, _, err := m.Match(vp, p) assert.NoError(t, err) assertMatches(t, expected, actual) } func assertMatches(t *testing.T, expected, actual []match.Match) { t.Helper() var opts = []cmp.Option{ cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.IgnoreFields(pkg.Package{}, "Locations"), cmpopts.IgnoreUnexported(distro.Distro{}), } if diff := cmp.Diff(expected, actual, opts...); diff != "" { t.Errorf("mismatch (-want +got):\n%s", diff) } } func Test_nakConstraint(t *testing.T) { tests := []struct { name string input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool }{ { name: "matches apk", input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, matches: true, }, { name: "not match due to type", input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 0", version.SemanticFormat), }, matches: false, }, { name: "not match", input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), }, matches: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } matches, _, err := nakConstraint.MatchesVulnerability(tt.input) tt.wantErr(t, err) require.Equal(t, tt.matches, matches) }) } } func Test_nakIgnoreRules(t *testing.T) { cases := []struct { name string pkgs []pkg.Package vulns []vulnerability.Vulnerability expectedLocationIgnores map[string][]string errAssertion assert.ErrorAssertionFunc }{ { name: "false positive in wolfi package adds index entry", pkgs: []pkg.Package{ { Name: "foo", Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-binary", }, }}, }, }, vulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "wolfi:distro:wolfi:rolling", }, PackageName: "foo", Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, }, expectedLocationIgnores: map[string][]string{ "/bin/foo-binary": {"GHSA-2014-fake-3"}, }, errAssertion: assert.NoError, }, { name: "false positive in wolfi subpackage adds index entry", pkgs: []pkg.Package{ { Name: "subpackage-foo", Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-subpackage-binary", }, }}, Upstreams: []pkg.UpstreamPackage{ { Name: "origin-foo", }, }, }, }, vulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "wolfi:distro:wolfi:rolling", }, PackageName: "origin-foo", Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, }, expectedLocationIgnores: map[string][]string{ "/bin/foo-subpackage-binary": {"GHSA-2014-fake-3"}, }, errAssertion: assert.NoError, }, { name: "fixed vuln (not a false positive) in wolfi package", pkgs: []pkg.Package{ { Name: "foo", Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-binary", }, }}, }, }, vulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "wolfi:distro:wolfi:rolling", }, PackageName: "foo", Constraint: version.MustGetConstraint("< 1.2.3-r4", version.ApkFormat), }, }, expectedLocationIgnores: map[string][]string{}, errAssertion: assert.NoError, }, { name: "no vuln data for wolfi package", pkgs: []pkg.Package{ { Name: "foo", Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ { Path: "/bin/foo-binary", }, }}, }, }, vulns: []vulnerability.Vulnerability{}, expectedLocationIgnores: map[string][]string{}, errAssertion: assert.NoError, }, { name: "no files listed for a wolfi package", pkgs: []pkg.Package{ { Name: "foo", Distro: &distro.Distro{Type: distro.Wolfi}, Metadata: pkg.ApkMetadata{Files: nil}, }, }, vulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "wolfi:distro:wolfi:rolling", }, PackageName: "foo", Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, }, expectedLocationIgnores: map[string][]string{}, errAssertion: assert.NoError, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { // create mock vulnerability provider vp := mock.VulnerabilityProvider(tt.vulns...) apkMatcher := &Matcher{} var allMatches []match.Match var allIgnores []match.IgnoreFilter for _, p := range tt.pkgs { matches, ignores, err := apkMatcher.Match(vp, p) require.NoError(t, err) allMatches = append(allMatches, matches...) allIgnores = append(allIgnores, ignores...) } actualResult := map[string][]string{} for _, ignore := range allIgnores { rule, ok := ignore.(match.IgnoreRule) if !ok { require.Fail(t, "expected ignore to be of type IgnoreRule") } if rule.Package.Location == "" { require.Fail(t, "expected package location to be set in ignore rule") } actualResult[rule.Package.Location] = append(actualResult[rule.Package.Location], rule.Vulnerability) } require.Equal(t, tt.expectedLocationIgnores, actualResult) }) } } func TestMatcherApk_DistroFixedIgnoreRules(t *testing.T) { apkNamespace := "secdb:distro:wolfi:rolling" apkFiles := pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ {Path: "/usr/bin/kyverno"}, {Path: "/usr/lib/kyverno/config"}, }} tests := []struct { name string p pkg.Package vulnerabilities []vulnerability.Vulnerability expectedIgnoreVulnIDs []string expectedMatchIDs []string }{ { name: "package already at fixed version - should produce location-scoped ignore rules but no matches", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kyverno", Version: "1.15.3-r0", Type: syftPkg.ApkPkg, Distro: distro.New(distro.Wolfi, "", ""), Metadata: apkFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "kyverno", Constraint: version.MustGetConstraint("< 1.15.3-r0", version.ApkFormat), Reference: vulnerability.Reference{ID: "CVE-2026-22039", Namespace: apkNamespace}, }, }, // one rule per owned path expectedIgnoreVulnIDs: []string{"CVE-2026-22039", "CVE-2026-22039"}, expectedMatchIDs: nil, }, { name: "package still vulnerable - should produce matches but no ignore rules", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kyverno", Version: "1.14.5-r0", Type: syftPkg.ApkPkg, Distro: distro.New(distro.Wolfi, "", ""), Metadata: apkFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "kyverno", Constraint: version.MustGetConstraint("< 1.15.3-r0", version.ApkFormat), Reference: vulnerability.Reference{ID: "CVE-2026-22039", Namespace: apkNamespace}, }, }, expectedIgnoreVulnIDs: nil, expectedMatchIDs: []string{"CVE-2026-22039"}, }, { name: "no distro data for the package - no ignore rules (search miss allows GHSA to stand)", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "something-obscure", Version: "1.0.0-r0", Type: syftPkg.ApkPkg, Distro: distro.New(distro.Wolfi, "", ""), Metadata: apkFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "kyverno", Constraint: version.MustGetConstraint("< 1.15.3-r0", version.ApkFormat), Reference: vulnerability.Reference{ID: "CVE-2026-22039", Namespace: apkNamespace}, }, }, expectedIgnoreVulnIDs: nil, expectedMatchIDs: nil, }, { name: "upstream package is fixed - should produce location-scoped ignore rules", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kyverno-cli", Version: "1.15.3-r0", Type: syftPkg.ApkPkg, Distro: distro.New(distro.Wolfi, "", ""), Metadata: apkFiles, Upstreams: []pkg.UpstreamPackage{ { Name: "kyverno", Version: "1.15.3-r0", }, }, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "kyverno", Constraint: version.MustGetConstraint("< 1.15.3-r0", version.ApkFormat), Reference: vulnerability.Reference{ID: "CVE-2026-22039", Namespace: apkNamespace}, }, }, // one rule per owned path expectedIgnoreVulnIDs: []string{"CVE-2026-22039", "CVE-2026-22039"}, expectedMatchIDs: nil, }, { name: "fixed CVE with related GHSA - ignore rules include both IDs at all paths", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kyverno", Version: "1.15.3-r0", Type: syftPkg.ApkPkg, Distro: distro.New(distro.Wolfi, "", ""), Metadata: apkFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "kyverno", Constraint: version.MustGetConstraint("< 1.15.3-r0", version.ApkFormat), Reference: vulnerability.Reference{ID: "CVE-2026-22039", Namespace: apkNamespace}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "GHSA-8p9x-46gm-qfx2", Namespace: "github:language:go"}, }, }, }, // 2 IDs × 2 paths = 4 rules expectedIgnoreVulnIDs: []string{"CVE-2026-22039", "CVE-2026-22039", "GHSA-8p9x-46gm-qfx2", "GHSA-8p9x-46gm-qfx2"}, expectedMatchIDs: nil, }, { name: "no APK metadata (no file list) - should NOT produce ignore rules even if fixed", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "kyverno", Version: "1.15.3-r0", Type: syftPkg.ApkPkg, Distro: distro.New(distro.Wolfi, "", ""), // no Metadata }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "kyverno", Constraint: version.MustGetConstraint("< 1.15.3-r0", version.ApkFormat), Reference: vulnerability.Reference{ID: "CVE-2026-22039", Namespace: apkNamespace}, }, }, expectedIgnoreVulnIDs: nil, expectedMatchIDs: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { matcher := Matcher{} store := mock.VulnerabilityProvider(test.vulnerabilities...) matches, ignoreFilters, err := matcher.Match(store, test.p) require.NoError(t, err) // verify matches var gotMatchIDs []string for _, m := range matches { gotMatchIDs = append(gotMatchIDs, m.Vulnerability.ID) } if test.expectedMatchIDs == nil { assert.Empty(t, gotMatchIDs, "expected no matches") } else { assert.ElementsMatch(t, test.expectedMatchIDs, gotMatchIDs, "unexpected match IDs") } // verify ignore rules - filter to only DistroPackageFixed rules (not NAK rules) var gotIgnoreIDs []string for _, filter := range ignoreFilters { rule, ok := filter.(match.IgnoreRule) require.True(t, ok, "expected IgnoreRule type") if rule.Reason != "DistroPackageFixed" { continue } gotIgnoreIDs = append(gotIgnoreIDs, rule.Vulnerability) assert.True(t, rule.IncludeAliases, "expected IncludeAliases to be true") assert.NotEmpty(t, rule.Package.Location, "expected location to be set on DistroPackageFixed rule") } if test.expectedIgnoreVulnIDs == nil { assert.Empty(t, gotIgnoreIDs, "expected no ignore rules") } else { assert.ElementsMatch(t, test.expectedIgnoreVulnIDs, gotIgnoreIDs, "unexpected ignore rule vulnerability IDs") } }) } } ================================================ FILE: grype/matcher/bitnami/matcher.go ================================================ package bitnami import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct{} func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.BitnamiPkg} } func (m *Matcher) Type() match.MatcherType { return match.BitnamiMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { // Bitnami packages' metadata are built from the package URL which contains // info such as the package name, version, revision, distro or architecture. // ref: https://github.com/anchore/syft/blob/main/syft/pkg/bitnami.go#L3-L13 // ref: https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/bitnami/package.go#L18-L45 return internal.MatchPackageByEcosystemPackageName(store, p, p.Name, m.Type()) } ================================================ FILE: grype/matcher/dotnet/matcher.go ================================================ package dotnet import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewDotnetMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.DotnetPkg} } func (m *Matcher) Type() match.MatcherType { return match.DotnetMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/dpkg/matcher.go ================================================ package dpkg import ( "errors" "fmt" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { MissingEpochStrategy version.MissingEpochStrategy UseCPEsForEOL bool } func NewDpkgMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.DebPkg} } func (m *Matcher) Type() match.MatcherType { return match.DpkgMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { matches := make([]match.Match, 0) sourceMatches, err := m.matchUpstreamPackages(store, p) if err != nil { return nil, nil, fmt.Errorf("failed to match by source indirection: %w", err) } matches = append(matches, sourceMatches...) versionConfig := version.ComparisonConfig{ MissingEpochStrategy: m.cfg.MissingEpochStrategy, } exactMatches, _, err := internal.MatchPackageByDistro(store, p, nil, m.Type(), &versionConfig) if err != nil { return nil, nil, fmt.Errorf("failed to match by exact package name: %w", err) } matches = append(matches, exactMatches...) // if configured, also search by CPEs for packages from EOL distros if m.cfg.UseCPEsForEOL && internal.IsDistroEOL(store, p.Distro) { log.WithFields("package", p.Name, "distro", p.Distro).Debug("distro is EOL, searching by CPEs") cpeMatches, err := internal.MatchPackageByCPEs(store, p, m.Type()) switch { case errors.Is(err, internal.ErrEmptyCPEMatch): log.WithFields("package", p.Name).Debug("package has no CPEs for EOL fallback matching") case err != nil: log.WithFields("package", p.Name, "error", err).Debug("failed to match by CPEs for EOL distro") default: matches = append(matches, cpeMatches...) } } return matches, nil, nil } func (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { var matches []match.Match versionConfig := version.ComparisonConfig{ MissingEpochStrategy: m.cfg.MissingEpochStrategy, } for _, indirectPackage := range pkg.UpstreamPackages(p) { indirectMatches, _, err := internal.MatchPackageByDistro(store, indirectPackage, &p, m.Type(), &versionConfig) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities for dpkg upstream source package: %w", err) } matches = append(matches, indirectMatches...) } // we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package) // however, we also want to keep the indirect package around for future reference match.ConvertToIndirectMatches(matches, p) return matches, nil } ================================================ FILE: grype/matcher/dpkg/matcher_mocks_test.go ================================================ package dpkg import ( "time" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" syftCpe "github.com/anchore/syft/syft/cpe" ) func newMockProvider() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { PackageName: "neutron", Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: "secdb:distro:debian:8"}, Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), }, // expected... { PackageName: "neutron-devel", Constraint: version.MustGetConstraint("< 2014.1.4-5", version.DebFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "secdb:distro:debian:8"}, }, { PackageName: "neutron-devel", Constraint: version.MustGetConstraint("< 2015.0.0-1", version.DebFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "secdb:distro:debian:8"}, }, // unexpected... { PackageName: "neutron-devel", Constraint: version.MustGetConstraint("< 2014.0.4-1", version.DebFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "secdb:distro:debian:8"}, }, }...) } // mockEOLProvider wraps mock.VulnerabilityProvider and adds EOLChecker support for testing type mockEOLProvider struct { vulnerability.Provider eolDate *time.Time } func (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) { return m.eolDate, nil, nil } func newMockEOLProvider(eolDate *time.Time) *mockEOLProvider { // include CPE vulnerability for testing CPE fallback return &mockEOLProvider{ Provider: mock.VulnerabilityProvider([]vulnerability.Vulnerability{ // distro-based vulnerability { PackageName: "openssl", Reference: vulnerability.Reference{ID: "CVE-2014-distro-1", Namespace: "secdb:distro:debian:8"}, Constraint: version.MustGetConstraint("< 1.0.2", version.DebFormat), }, // CPE-based vulnerability { PackageName: "openssl", Reference: vulnerability.Reference{ID: "CVE-2014-cpe-1", Namespace: "nvd:cpe"}, Constraint: version.MustGetConstraint("< 1.0.2", version.UnknownFormat), CPEs: []syftCpe.CPE{ syftCpe.Must("cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*", ""), }, }, }...), eolDate: eolDate, } } ================================================ FILE: grype/matcher/dpkg/matcher_test.go ================================================ package dpkg import ( "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/stringutil" syftCpe "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { matcher := Matcher{} d := distro.New(distro.Debian, "8", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "2014.1.3-6", Type: syftPkg.DebPkg, Distro: d, Upstreams: []pkg.UpstreamPackage{ { Name: "neutron-devel", }, }, } vp := newMockProvider() actual, err := matcher.matchUpstreamPackages(vp, p) assert.NoError(t, err, "unexpected err from matchUpstreamPackages", err) assert.Len(t, actual, 2, "unexpected indirect matches count") foundCVEs := stringutil.NewStringSet() for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) require.NotEmpty(t, a.Details) for _, d := range a.Details { assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated") } assert.Equal(t, p.Name, a.Package.Name, "failed to capture original package name") for _, detail := range a.Details { assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type") } } for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} { if !foundCVEs.Contains(id) { t.Errorf("missing discovered CVE: %s", id) } } if t.Failed() { t.Logf("discovered CVES: %+v", foundCVEs) } } func TestMatcherDpkg_CPEFallbackWhenEOL(t *testing.T) { pastEOL := time.Now().AddDate(-1, 0, 0) // 1 year ago futureEOL := time.Now().AddDate(1, 0, 0) // 1 year from now d := distro.New(distro.Debian, "8", "") // package with CPEs for CPE-based matching p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "openssl", Version: "1.0.1", Type: syftPkg.DebPkg, Distro: d, CPEs: []syftCpe.CPE{ syftCpe.Must("cpe:2.3:a:openssl:openssl:1.0.1:*:*:*:*:*:*:*", ""), }, } tests := []struct { name string useCPEsForEOL bool eolDate *time.Time expectCPEMatches bool }{ { name: "CPE fallback enabled and distro is EOL - should include CPE matches", useCPEsForEOL: true, eolDate: &pastEOL, expectCPEMatches: true, }, { name: "CPE fallback enabled but distro not EOL - should not include CPE matches", useCPEsForEOL: true, eolDate: &futureEOL, expectCPEMatches: false, }, { name: "CPE fallback disabled and distro is EOL - should not include CPE matches", useCPEsForEOL: false, eolDate: &pastEOL, expectCPEMatches: false, }, { name: "CPE fallback disabled and distro not EOL - should not include CPE matches", useCPEsForEOL: false, eolDate: &futureEOL, expectCPEMatches: false, }, { name: "CPE fallback enabled but no EOL data - should not include CPE matches", useCPEsForEOL: true, eolDate: nil, expectCPEMatches: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { matcher := NewDpkgMatcher(MatcherConfig{ UseCPEsForEOL: tt.useCPEsForEOL, }) vp := newMockEOLProvider(tt.eolDate) matches, _, err := matcher.Match(vp, p) require.NoError(t, err) // check if any CPE matches were found hasCPEMatch := false for _, m := range matches { for _, detail := range m.Details { if detail.Type == match.CPEMatch { hasCPEMatch = true break } } } if tt.expectCPEMatches { assert.True(t, hasCPEMatch, "expected CPE matches for EOL distro") } else { assert.False(t, hasCPEMatch, "did not expect CPE matches") } }) } } ================================================ FILE: grype/matcher/golang/matcher.go ================================================ package golang import ( "strings" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool AlwaysUseCPEForStdlib bool AllowMainModulePseudoVersionComparison bool } func NewGolangMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.GoModulePkg} } func (m *Matcher) Type() match.MatcherType { return match.GoModuleMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { matches := make([]match.Match, 0) mainModule := "" if m, ok := p.Metadata.(pkg.GolangBinMetadata); ok { mainModule = m.MainModule } // Golang currently does not have a standard way of incorporating the main // module's version into the compiled binary: // https://github.com/golang/go/issues/50603. // // Syft has some fallback mechanisms to come up with a more sane version value // depending on the scenario. But if none of these apply, the Go-set value of // "(devel)" is used, which is altogether unhelpful for vulnerability matching. var isNotCorrected bool if m.cfg.AllowMainModulePseudoVersionComparison { isNotCorrected = strings.HasPrefix(p.Version, "(devel)") } else { // when AllowPseudoVersionComparison is false isNotCorrected = strings.HasPrefix(p.Version, "v0.0.0-") || strings.HasPrefix(p.Version, "(devel)") } if p.Name == mainModule && isNotCorrected { return matches, nil, nil } return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), searchByCPE(p.Name, m.cfg)) } func searchByCPE(name string, cfg MatcherConfig) bool { if cfg.UseCPEs { return true } return cfg.AlwaysUseCPEForStdlib && (name == "stdlib") } ================================================ FILE: grype/matcher/golang/matcher_test.go ================================================ package golang import ( "testing" "github.com/google/uuid" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcher_DropMainPackageGivenVersionInfo(t *testing.T) { tests := []struct { name string subjectWithoutMainModule pkg.Package mainModuleData pkg.GolangBinMetadata allowPsuedoVersionComparison bool expectedMatchCount int }{ { name: "main module with version is matched when pseudo version comparison is allowed", subjectWithoutMainModule: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "istio.io/istio", Version: "v0.0.0-20220606222826-f59ce19ec6b6", Type: syftPkg.GoModulePkg, Language: syftPkg.Go, Metadata: pkg.GolangBinMetadata{}, }, mainModuleData: pkg.GolangBinMetadata{ MainModule: "istio.io/istio", }, allowPsuedoVersionComparison: true, expectedMatchCount: 1, }, { name: "main module with version is NOT matched when pseudo version comparison is disabled", subjectWithoutMainModule: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "istio.io/istio", Version: "v0.0.0-20220606222826-f59ce19ec6b6", Type: syftPkg.GoModulePkg, Language: syftPkg.Go, Metadata: pkg.GolangBinMetadata{}, }, mainModuleData: pkg.GolangBinMetadata{ MainModule: "istio.io/istio", }, allowPsuedoVersionComparison: false, expectedMatchCount: 0, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { mainModuleMetadata := test.mainModuleData subjectWithoutMainModule := test.subjectWithoutMainModule subjectWithMainModule := subjectWithoutMainModule subjectWithMainModule.Metadata = mainModuleMetadata subjectWithMainModuleAsDevel := subjectWithMainModule subjectWithMainModuleAsDevel.Version = "(devel)" matcher := NewGolangMatcher(MatcherConfig{ AllowMainModulePseudoVersionComparison: test.allowPsuedoVersionComparison, }) store := newMockProvider() preTest, _, _ := matcher.Match(store, subjectWithoutMainModule) assert.Len(t, preTest, 1, "should have matched the package when there is not a main module") actual, _, _ := matcher.Match(store, subjectWithMainModule) assert.Len(t, actual, test.expectedMatchCount, "should match the main module depending on config (i.e. 1 match)") actual, _, _ = matcher.Match(store, subjectWithMainModuleAsDevel) assert.Len(t, actual, 0, "unexpected match count; should never match main module (devel)") }) } } func TestMatcher_SearchForStdlib(t *testing.T) { // values derived from: // $ go version -m $(which grype) // /opt/homebrew/bin/grype: go1.21.1 subject := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "stdlib", Version: "go1.18.3", Type: syftPkg.GoModulePkg, Language: syftPkg.Go, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*", ""), }, Metadata: pkg.GolangBinMetadata{}, } cases := []struct { name string cfg MatcherConfig subject pkg.Package expectedCVEs []string }{ // positive { name: "cpe enables, no override enabled", cfg: MatcherConfig{ UseCPEs: true, AlwaysUseCPEForStdlib: false, }, subject: subject, expectedCVEs: []string{ "CVE-2022-27664", }, }, { name: "stdlib search, cpe enables, no override enabled", cfg: MatcherConfig{ UseCPEs: true, AlwaysUseCPEForStdlib: true, }, subject: subject, expectedCVEs: []string{ "CVE-2022-27664", }, }, { name: "stdlib search, cpe enables, no override enabled", cfg: MatcherConfig{ UseCPEs: false, AlwaysUseCPEForStdlib: true, }, subject: subject, expectedCVEs: []string{ "CVE-2022-27664", }, }, { name: "go package search should be found by cpe", cfg: MatcherConfig{ UseCPEs: true, AlwaysUseCPEForStdlib: true, }, subject: func() pkg.Package { p := subject; p.Name = "go"; return p }(), expectedCVEs: []string{ "CVE-2022-27664", }}, // negative { name: "stdlib search, cpe suppressed, no override enabled", cfg: MatcherConfig{ UseCPEs: false, AlwaysUseCPEForStdlib: false, }, subject: subject, expectedCVEs: nil, }, { name: "go package search should not be an exception (only the stdlib)", cfg: MatcherConfig{ UseCPEs: false, AlwaysUseCPEForStdlib: true, }, subject: func() pkg.Package { p := subject; p.Name = "go"; return p }(), expectedCVEs: nil, }, } store := newMockProvider() for _, c := range cases { t.Run(c.name, func(t *testing.T) { matcher := NewGolangMatcher(c.cfg) actual, _, _ := matcher.Match(store, c.subject) actualCVEs := strset.New() for _, m := range actual { actualCVEs.Add(m.Vulnerability.ID) } expectedCVEs := strset.New(c.expectedCVEs...) assert.ElementsMatch(t, expectedCVEs.List(), actualCVEs.List()) }) } } func newMockProvider() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ // for TestMatcher_DropMainPackageIfNoVersion { PackageName: "istio.io/istio", Constraint: version.MustGetConstraint("< 5.0.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "github:language:" + syftPkg.Go.String()}, }, { CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*", "test")}, Constraint: version.MustGetConstraint("< 1.18.6 || = 1.19.0", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2022-27664", Namespace: "nvd:cpe"}, }, }...) } ================================================ FILE: grype/matcher/hex/matcher.go ================================================ package hex import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewHexMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.HexPkg, syftPkg.ErlangOTPPkg} } func (m *Matcher) Type() match.MatcherType { return match.HexMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/internal/common.go ================================================ package internal import ( "errors" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) func MatchPackageByEcosystemAndCPEs(store vulnerability.Provider, p pkg.Package, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match var ignored []match.IgnoreFilter for _, name := range store.PackageSearchNames(p) { nameMatches, nameIgnores, err := MatchPackageByEcosystemPackageNameAndCPEs(store, p, name, matcher, includeCPEs) if err != nil { return nil, nil, err } matches = append(matches, nameMatches...) ignored = append(ignored, nameIgnores...) } return matches, ignored, nil } func MatchPackageByEcosystemPackageNameAndCPEs(store vulnerability.Provider, p pkg.Package, packageName string, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoreFilter, error) { matches, ignored, err := MatchPackageByEcosystemPackageName(store, p, packageName, matcher) if err != nil { log.Debugf("could not match by package ecosystem (package=%+v): %v", p, err) } if includeCPEs { cpeMatches, err := MatchPackageByCPEs(store, p, matcher) if errors.Is(err, ErrEmptyCPEMatch) { log.Debugf("attempted CPE search on %s, which has no CPEs. Consider re-running with --add-cpes-if-none", p.Name) } else if err != nil { log.Debugf("could not match by package CPE (package=%+v): %v", p, err) } matches = append(matches, cpeMatches...) } return matches, ignored, nil } ================================================ FILE: grype/matcher/internal/cpe.go ================================================ package internal import ( "errors" "fmt" "sort" "strings" "github.com/facebookincubator/nvdtools/wfn" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func alpineCPEComparableVersion(version string) string { // clean the alpine package version so that it compares correctly with the CPE version comparison logic // alpine versions are suffixed with -r{buildindex}; however, if left intact CPE comparison logic will // incorrectly treat these as a pre-release. In actuality, we just want to treat 1.2.3-r21 as equivalent to // 1.2.3 for purposes of CPE-based matching since the alpine fix should filter out any cases where a later // build fixes something that was vulnerable in 1.2.3 components := strings.Split(version, "-r") cpeComparableVersion := version if len(components) == 2 { cpeComparableVersion = components[0] } return cpeComparableVersion } var ErrEmptyCPEMatch = errors.New("attempted CPE match against package with no CPEs") // MatchPackageByCPEs retrieves all vulnerabilities that match any of the provided package's CPEs func MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) { // we attempt to merge match details within the same matcher when searching by CPEs, in this way there are fewer duplicated match // objects (and fewer duplicated match details). // Warn the user if they are matching by CPE, but there are no CPEs available. if len(p.CPEs) == 0 { return nil, ErrEmptyCPEMatch } matchesByFingerprint := make(map[match.Fingerprint]match.Match) for _, c := range p.CPEs { // prefer the CPE version, but if npt specified use the package version searchVersion := c.Attributes.Version if p.Type == syftPkg.ApkPkg { searchVersion = alpineCPEComparableVersion(searchVersion) } if searchVersion == wfn.NA || searchVersion == wfn.Any || isUnknownVersion(searchVersion) { searchVersion = p.Version } if isUnknownVersion(searchVersion) { log.WithFields("package", p.Name).Trace("skipping package with unknown version") continue } // we should always show the exact CPE we searched by, not just what's in the component analysis (since we // may alter the version based on above processing) c.Attributes.Version = searchVersion format := pkg.VersionFormat(p) if format == version.JVMFormat { searchVersion = transformJvmVersion(searchVersion, c.Attributes.Update) } var verObj *version.Version var err error if searchVersion != "" { verObj = version.New(searchVersion, format) } // find all vulnerability records in the DB for the given CPE (not including version comparisons) vulns, err := provider.FindVulnerabilities( search.ByCPE(c), OnlyVulnerableTargets(p), OnlyQualifiedPackages(p), OnlyVulnerableVersions(verObj), OnlyNonWithdrawnVulnerabilities(), ) if err != nil { return nil, fmt.Errorf("matcher failed to fetch by CPE pkg=%q: %w", p.Name, err) } // for each vulnerability record found, check the version constraint. If the constraint is satisfied // relative to the current version information from the CPE (or the package) then the given package // is vulnerable. for _, vuln := range vulns { addNewMatch(matchesByFingerprint, vuln, p, verObj, upstreamMatcher, c) } } return toMatches(matchesByFingerprint), nil } func transformJvmVersion(searchVersion, updateCpeField string) string { // we should take into consideration the CPE update field for JVM packages if strings.HasPrefix(searchVersion, "1.") && !strings.Contains(searchVersion, "_") && updateCpeField != wfn.NA && updateCpeField != wfn.Any { searchVersion = fmt.Sprintf("%s_%s", searchVersion, strings.TrimPrefix(updateCpeField, "update")) } return searchVersion } func addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, searchVersion *version.Version, upstreamMatcher match.MatcherType, searchedByCPE cpe.CPE) { candidateMatch := match.Match{ Vulnerability: vuln, Package: p, } if existingMatch, exists := matchesByFingerprint[candidateMatch.Fingerprint()]; exists { candidateMatch = existingMatch } candidateMatch.Details = addMatchDetails(candidateMatch.Details, CPEMatchDetails(upstreamMatcher, vuln, searchedByCPE, p, searchVersion), ) matchesByFingerprint[candidateMatch.Fingerprint()] = candidateMatch } func CPEMatchDetails(matcherType match.MatcherType, vuln vulnerability.Vulnerability, searchedByCPE cpe.CPE, p pkg.Package, searchVersion *version.Version) match.Detail { return match.Detail{ Type: match.CPEMatch, Confidence: 0.9, // TODO: this is hard coded for now Matcher: matcherType, SearchedBy: match.CPEParameters{ Namespace: vuln.Namespace, CPEs: []string{ // use .String() for proper escaping searchedByCPE.Attributes.String(), }, Package: match.PackageParameter{ Name: p.Name, Version: p.Version, }, }, Found: match.CPEResult{ VulnerabilityID: vuln.ID, VersionConstraint: vuln.Constraint.String(), CPEs: cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)), }, } } func addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) []match.Detail { newFound, ok := newDetails.Found.(match.CPEResult) if !ok { return existingDetails } newSearchedBy, ok := newDetails.SearchedBy.(match.CPEParameters) if !ok { return existingDetails } for idx, detail := range existingDetails { found, ok := detail.Found.(match.CPEResult) if !ok { continue } searchedBy, ok := detail.SearchedBy.(match.CPEParameters) if !ok { continue } if !found.Equals(newFound) { continue } err := searchedBy.Merge(newSearchedBy) if err != nil { continue } existingDetails[idx].SearchedBy = searchedBy return existingDetails } // could not merge with another entry, append to the end existingDetails = append(existingDetails, newDetails) return existingDetails } func filterCPEsByVersion(pkgVersion *version.Version, allCPEs []cpe.CPE) (matchedCPEs []cpe.CPE) { if pkgVersion == nil { // all CPEs are valid in the case when a version is not specified return allCPEs } for _, c := range allCPEs { if c.Attributes.Version == wfn.Any || c.Attributes.Version == wfn.NA { matchedCPEs = append(matchedCPEs, c) continue } ver := c.Attributes.Version if pkgVersion.Format == version.JVMFormat { if c.Attributes.Update != wfn.Any && c.Attributes.Update != wfn.NA { ver = transformJvmVersion(ver, c.Attributes.Update) } } constraint, err := version.GetConstraint(ver, pkgVersion.Format) if err != nil { // if we can't get a version constraint, don't filter out the CPE matchedCPEs = append(matchedCPEs, c) continue } satisfied, err := constraint.Satisfied(pkgVersion) if err != nil || satisfied { // if we can't check for version satisfaction, don't filter out the CPE matchedCPEs = append(matchedCPEs, c) continue } } return matchedCPEs } func toMatches(matchesByFingerprint map[match.Fingerprint]match.Match) (matches []match.Match) { for _, m := range matchesByFingerprint { matches = append(matches, m) } sort.Sort(match.ByElements(matches)) return matches } // cpesToString receives one or more CPEs and stringifies them func cpesToString(cpes []cpe.CPE) []string { var strs = make([]string, len(cpes)) for idx, c := range cpes { // use .String() for proper escaping strs[idx] = c.Attributes.String() } sort.Strings(strs) return strs } ================================================ FILE: grype/matcher/internal/cpe_test.go ================================================ package internal import ( "errors" "testing" "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func newCPETestStore() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-1", Namespace: "nvd:cpe", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.GemFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-2", Namespace: "nvd:cpe", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-3", Namespace: "nvd:cpe", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("= 4.0.1", version.GemFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-4", Namespace: "nvd:cpe", }, PackageName: "awesome", Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", ""), }, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-5", Namespace: "nvd:cpe", }, PackageName: "multiple", Constraint: version.MustGetConstraint("< 4.0", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*", ""), }, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-6", Namespace: "nvd:cpe", }, PackageName: "funfun", Constraint: version.MustGetConstraint("= 5.2.1", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""), }, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-7", Namespace: "nvd:cpe", }, PackageName: "sw", Constraint: version.MustGetConstraint("< 1.0", version.UnknownFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-2021-23369", Namespace: "nvd:cpe", }, PackageName: "handlebars", Constraint: version.MustGetConstraint("< 4.7.7", version.UnknownFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", "")}, }, }...) } func TestFindMatchesByPackageCPE(t *testing.T) { matcher := match.RubyGemMatcher tests := []struct { name string p pkg.Package expected []match.Match wantErr require.ErrorAssertionFunc }{ { name: "match from range", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.5", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-1"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.5", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"}, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (gem)", VulnerabilityID: "CVE-2017-fake-1", }, Matcher: matcher, }, }, }, }, }, { name: "fallback to package version", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.5", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-1"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:unknown:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.5", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*"}, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (gem)", VulnerabilityID: "CVE-2017-fake-1", }, Matcher: matcher, }, }, }, }, }, { name: "return all possible matches when missing version", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-1"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "", // important! Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", //important! }, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "activerecord", Version: "", // important! }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (gem)", VulnerabilityID: "CVE-2017-fake-1", }, Matcher: matcher, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-2"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "", // important! Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*"}, //important! Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "activerecord", Version: "", // important! }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"}, VersionConstraint: "< 3.7.4 (gem)", VulnerabilityID: "CVE-2017-fake-2", }, Matcher: matcher, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-3"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "", // important! Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", //important! "cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", //important! }, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "activerecord", Version: "", // important! }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"}, VersionConstraint: "= 4.0.1 (gem)", VulnerabilityID: "CVE-2017-fake-3", }, Matcher: matcher, }, }, }, }, }, { name: "suppress matching when version is unknown", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "unknown", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{}, }, { name: "multiple matches", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.3", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-1"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.3", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", }, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.3", }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"}, VersionConstraint: "< 3.7.6 (gem)", VulnerabilityID: "CVE-2017-fake-1", }, Matcher: matcher, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-2"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*", ""), }, Name: "activerecord", Version: "3.7.3", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.3", }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*"}, VersionConstraint: "< 3.7.4 (gem)", VulnerabilityID: "CVE-2017-fake-2", }, Matcher: matcher, }, }, }, }, }, { name: "exact match", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*", ""), }, Name: "activerecord", Version: "4.0.1", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-3"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*", ""), }, Name: "activerecord", Version: "4.0.1", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "activerecord", Version: "4.0.1", }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*"}, VersionConstraint: "= 4.0.1 (gem)", VulnerabilityID: "CVE-2017-fake-3", }, Matcher: matcher, }, }, }, }, }, { name: "no match", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "couldntgetthisrightcouldyou", Version: "4.0.1", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:no_match:no_match:0.9.9:*:*:*:*:*:*:*", cpe.GeneratedSource), }, }, expected: []match.Match{}, }, { name: "fuzzy version match", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*", ""), }, Name: "awesome", Version: "98SE1", }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-4"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*", ""), }, Name: "awesome", Version: "98SE1", }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "awesome", Version: "98SE1", }, }, Found: match.CPEResult{ CPEs: []string{"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*"}, VersionConstraint: "< 98SP3 (unknown)", VulnerabilityID: "CVE-2017-fake-4", }, Matcher: matcher, }, }, }, }, }, { name: "multiple matched CPEs", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""), }, Name: "multiple", Version: "1.0", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-5"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", ""), }, Name: "multiple", Version: "1.0", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "multiple", Version: "1.0", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, VersionConstraint: "< 4.0 (unknown)", VulnerabilityID: "CVE-2017-fake-5", }, Matcher: matcher, }, }, }, }, }, { name: "filtered out match due to target_sw mismatch", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:*:*:*", ""), }, Name: "funfun", Version: "5.2.1", Language: syftPkg.Rust, // this is identified as a rust package Type: syftPkg.RustPkg, }, expected: []match.Match{}, }, { name: "target_sw mismatch with unsupported target_sw", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*", ""), }, Name: "sw", Version: "0.1", Language: syftPkg.Erlang, Type: syftPkg.HexPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-7"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*", ""), }, Name: "sw", Version: "0.1", Language: syftPkg.Erlang, Type: syftPkg.HexPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:sw:sw:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "sw", Version: "0.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*", }, VersionConstraint: "< 1.0 (unknown)", VulnerabilityID: "CVE-2017-fake-7", }, Matcher: matcher, }, }, }, }, }, { name: "match included even though multiple cpes are mismatch", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""), }, Name: "funfun", Version: "5.2.1", Language: syftPkg.Python, Type: syftPkg.PythonPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2017-fake-6"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*", ""), cpe.Must("cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", ""), }, Name: "funfun", Version: "5.2.1", Language: syftPkg.Python, Type: syftPkg.PythonPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "funfun", Version: "5.2.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*", "cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*", }, VersionConstraint: "= 5.2.1 (unknown)", VulnerabilityID: "CVE-2017-fake-6", }, Matcher: matcher, }, }, }, }, }, { name: "Ensure target_sw mismatch does not apply to java packages", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.Java, Type: syftPkg.JavaPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2021-23369"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.Java, Type: syftPkg.JavaPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "handlebars", Version: "0.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, VersionConstraint: "< 4.7.7 (unknown)", VulnerabilityID: "CVE-2021-23369", }, Matcher: matcher, }, }, }, }, }, { name: "Ensure target_sw mismatch does not apply to java jenkins plugins packages", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.Java, Type: syftPkg.JenkinsPluginPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2021-23369"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.Java, Type: syftPkg.JenkinsPluginPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "handlebars", Version: "0.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, VersionConstraint: "< 4.7.7 (unknown)", VulnerabilityID: "CVE-2021-23369", }, Matcher: matcher, }, }, }, }, }, { name: "Ensure target_sw mismatch does not apply to binary packages", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.UnknownLanguage, Type: syftPkg.BinaryPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2021-23369"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.UnknownLanguage, Type: syftPkg.BinaryPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "handlebars", Version: "0.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, VersionConstraint: "< 4.7.7 (unknown)", VulnerabilityID: "CVE-2021-23369", }, Matcher: matcher, }, }, }, }, }, { name: "Ensure target_sw mismatch does not apply to unknown packages", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.UnknownLanguage, Type: syftPkg.UnknownPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2021-23369"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.UnknownLanguage, Type: syftPkg.UnknownPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "handlebars", Version: "0.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, VersionConstraint: "< 4.7.7 (unknown)", VulnerabilityID: "CVE-2021-23369", }, Matcher: matcher, }, }, }, }, }, { name: "package without CPEs returns error", p: pkg.Package{ Name: "some-package", }, expected: nil, wantErr: func(t require.TestingT, err error, i ...interface{}) { if !errors.Is(err, ErrEmptyCPEMatch) { t.Errorf("expected %v but got %v", ErrEmptyCPEMatch, err) t.FailNow() } }, }, { name: "Ensure match is kept for target software that matches the syft package language type", p: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.JavaScript, Type: syftPkg.NpmPkg, }, expected: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2021-23369"}, }, Package: pkg.Package{ CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*", ""), }, Name: "handlebars", Version: "0.1", Language: syftPkg.JavaScript, Type: syftPkg.NpmPkg, }, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ CPEs: []string{"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*"}, Namespace: "nvd:cpe", Package: match.PackageParameter{ Name: "handlebars", Version: "0.1", }, }, Found: match.CPEResult{ CPEs: []string{ "cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*", }, VersionConstraint: "< 4.7.7 (unknown)", VulnerabilityID: "CVE-2021-23369", }, Matcher: matcher, }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := MatchPackageByCPEs(newCPETestStore(), test.p, matcher) if test.wantErr == nil { test.wantErr = require.NoError } test.wantErr(t, err) assertMatchesUsingIDsForVulnerabilities(t, test.expected, actual) for idx, e := range test.expected { if idx < len(actual) { if d := cmp.Diff(e.Details, actual[idx].Details); d != "" { t.Errorf("unexpected match details (-want +got):\n%s", d) } } else { t.Errorf("expected match details (-want +got)\n%+v:\n", e.Details) } } }) } } func TestFilterCPEsByVersion(t *testing.T) { tests := []struct { name string version string vulnerabilityCPEs []string expected []string }{ { name: "filter out by simple version", version: "1.0", vulnerabilityCPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", }, expected: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, { name: "do not filter on empty version", version: "", // important! vulnerabilityCPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", }, expected: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", "cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // format strings to CPE objects... vulnerabilityCPEs := make([]cpe.CPE, len(test.vulnerabilityCPEs)) for idx, c := range test.vulnerabilityCPEs { vulnerabilityCPEs[idx] = cpe.Must(c, "") } var versionObj *version.Version if test.version != "" { versionObj = version.New(test.version, version.UnknownFormat) } // run the test subject... actual := filterCPEsByVersion(versionObj, vulnerabilityCPEs) // format CPE objects to string... actualStrs := make([]string, len(actual)) for idx, a := range actual { // use .String() for proper escaping actualStrs[idx] = a.Attributes.String() } assert.ElementsMatch(t, test.expected, actualStrs) }) } } func TestAddMatchDetails(t *testing.T) { tests := []struct { name string existing []match.Detail new match.Detail expected []match.Detail }{ { name: "append new entry -- found not equal", existing: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, new: match.Detail{ SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "totally-different-search", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "totally-different-match", }, }, }, expected: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "totally-different-search", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "totally-different-match", }, }, }, }, }, { name: "append new entry -- searchedBy merge fails", existing: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, new: match.Detail{ SearchedBy: match.CPEParameters{ Namespace: "totally-different", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, expected: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, { SearchedBy: match.CPEParameters{ Namespace: "totally-different", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, }, { name: "merge with exiting entry", existing: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, new: match.Detail{ SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "totally-different-search", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, expected: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", "totally-different-search", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, }, { name: "no addition - bad new searchedBy type", existing: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, new: match.Detail{ SearchedBy: "something else!", Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, expected: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, }, { name: "no addition - bad new found type", existing: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, new: match.Detail{ SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: "something-else!", }, expected: []match.Detail{ { SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*", }, }, Found: match.CPEResult{ VersionConstraint: "< 2.0 (unknown)", CPEs: []string{ "cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*", }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert.Equal(t, test.expected, addMatchDetails(test.existing, test.new)) }) } } func TestCPESearchHit_Equals(t *testing.T) { tests := []struct { name string current match.CPEResult other match.CPEResult expected bool }{ { name: "different version constraint", current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, other: match.CPEResult{ VersionConstraint: "different-constraint", CPEs: []string{ "a-cpe", }, }, expected: false, }, { name: "different number of CPEs", current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, other: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", "b-cpe", }, }, expected: false, }, { name: "different CPE value", current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, other: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "b-cpe", }, }, expected: false, }, { name: "matches", current: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, other: match.CPEResult{ VersionConstraint: "current-constraint", CPEs: []string{ "a-cpe", }, }, expected: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert.Equal(t, test.expected, test.current.Equals(test.other)) }) } } ================================================ FILE: grype/matcher/internal/distro.go ================================================ package internal import ( "fmt" "slices" "strings" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) func MatchPackageByDistro(provider vulnerability.Provider, searchPkg pkg.Package, catalogPkg *pkg.Package, upstreamMatcher match.MatcherType, cfg *version.ComparisonConfig) ([]match.Match, []match.IgnoreFilter, error) { if searchPkg.Distro == nil { return nil, nil, nil } if isUnknownVersion(searchPkg.Version) { log.WithFields("package", searchPkg.Name).Trace("skipping package with unknown version") return nil, nil, nil } var matches []match.Match // Create version with config embedded if provided var pkgVersion *version.Version if cfg != nil { pkgVersion = version.NewWithConfig(searchPkg.Version, pkg.VersionFormat(searchPkg), *cfg) } else { pkgVersion = version.New(searchPkg.Version, pkg.VersionFormat(searchPkg)) } versionCriteria := OnlyVulnerableVersions(pkgVersion) vulns, err := provider.FindVulnerabilities( search.ByPackageName(searchPkg.Name), search.ByDistro(*searchPkg.Distro), OnlyQualifiedPackages(searchPkg), versionCriteria, ) if err != nil { return nil, nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err) } for _, vuln := range vulns { matches = append(matches, match.Match{ Vulnerability: vuln, Package: matchPackage(searchPkg, catalogPkg), Details: distroMatchDetails(upstreamMatcher, searchPkg, catalogPkg, vuln), }) } return matches, nil, err } // MatchPackageByDistroWithOwnedFiles searches for all vulnerabilities the distro knows about for a // package in a single query, then partitions the results in memory into vulnerable matches and // location-scoped ignore rules for fixed vulnerabilities. The ignore rules are scoped to files // owned by the package so they only suppress findings for co-located packages. // // Owned files are discovered by checking whether the package metadata (on either catalogPkg or // searchPkg) implements [pkg.FileOwner]. When no owned files are available, this falls back to // [MatchPackageByDistro] (version-filtered query, no ignore rules) to avoid over-fetching. func MatchPackageByDistroWithOwnedFiles(provider vulnerability.Provider, searchPkg pkg.Package, catalogPkg *pkg.Package, upstreamMatcher match.MatcherType, cfg *version.ComparisonConfig) ([]match.Match, []match.IgnoreFilter, error) { // Use the SBOM package (not the synthetic upstream) for file ownership — the upstream // package won't carry file metadata. ownedFiles := ownedFilesFor(matchPackage(searchPkg, catalogPkg)) if len(ownedFiles) == 0 { return MatchPackageByDistro(provider, searchPkg, catalogPkg, upstreamMatcher, cfg) } if searchPkg.Distro == nil { return nil, nil, nil } if isUnknownVersion(searchPkg.Version) { log.WithFields("package", searchPkg.Name).Trace("skipping package with unknown version") return nil, nil, nil } // Create version with config embedded if provided var pkgVersion *version.Version if cfg != nil { pkgVersion = version.NewWithConfig(searchPkg.Version, pkg.VersionFormat(searchPkg), *cfg) } else { pkgVersion = version.New(searchPkg.Version, pkg.VersionFormat(searchPkg)) } versionCriteria := OnlyVulnerableVersions(pkgVersion) // Fetch all vulnerabilities the distro knows about for this package (1 query, no version filter). rp := result.NewProvider(provider, matchPackage(searchPkg, catalogPkg), upstreamMatcher) allVulns, err := rp.FindResults( search.ByPackageName(searchPkg.Name), search.ByDistro(*searchPkg.Distro), OnlyQualifiedPackages(searchPkg), ) if err != nil { return nil, nil, fmt.Errorf("matcher failed to fetch distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err) } // Split in memory: vulnerable vs. fixed. vulnerable := allVulns.Filter(versionCriteria) fixed := allVulns.Remove(vulnerable) // The superset query omits version criteria, so match details are missing the searched-by // version. Patch it in from the search package before converting to matches. patchDetailVersion(vulnerable, searchPkg.Version) matches := vulnerable.ToMatches() ignores := distroFixedIgnoreRules(fixed, ownedFiles) return matches, ignores, nil } // distroFixedIgnoreRules builds location-scoped ignore rules for vulnerabilities that the distro has // assessed as fixed. Each vulnerability ID (including aliases) gets one rule per owned path. func distroFixedIgnoreRules(fixed result.Set, ownedFiles []string) []match.IgnoreFilter { var ignores []match.IgnoreFilter for _, results := range fixed { for _, r := range results { for _, v := range r.Vulnerabilities { ids := collectVulnerabilityIDs(v) for _, id := range ids { for _, path := range ownedFiles { ignores = append(ignores, match.IgnoreRule{ Vulnerability: id, IncludeAliases: true, Reason: "DistroPackageFixed", Package: match.IgnoreRulePackage{ Location: path, }, }) } } } } } return ignores } func matchPackage(searchPkg pkg.Package, catalogPkg *pkg.Package) pkg.Package { if catalogPkg != nil { return *catalogPkg } return searchPkg } func distroMatchDetails(upstreamMatcher match.MatcherType, searchPkg pkg.Package, catalogPkg *pkg.Package, vuln vulnerability.Vulnerability) []match.Detail { ty := match.ExactIndirectMatch if catalogPkg == nil { ty = match.ExactDirectMatch } return []match.Detail{ { Type: ty, Matcher: upstreamMatcher, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: searchPkg.Distro.Type.String(), Version: searchPkg.Distro.Version, }, Package: match.PackageParameter{ Name: searchPkg.Name, Version: searchPkg.Version, }, Namespace: vuln.Namespace, }, Found: match.DistroResult{ VulnerabilityID: vuln.ID, VersionConstraint: vuln.Constraint.String(), }, Confidence: 1.0, // TODO: this is hard coded for now }, } } // collectVulnerabilityIDs returns the primary ID plus all related/alias IDs for a vulnerability. func collectVulnerabilityIDs(v vulnerability.Vulnerability) []string { ids := []string{v.ID} for _, related := range v.RelatedVulnerabilities { if !slices.Contains(ids, related.ID) { ids = append(ids, related.ID) } } return ids } func isUnknownVersion(v string) bool { return strings.ToLower(v) == "unknown" } // patchDetailVersion fills in the searched-by package version on match details that are missing it. // This is needed when results come from a superset query (no version criteria), since // result.Provider only populates the version from VersionCriteria in the query. func patchDetailVersion(s result.Set, version string) { for _, results := range s { for i := range results { for j := range results[i].Details { d := &results[i].Details[j] switch sb := d.SearchedBy.(type) { case match.DistroParameters: if sb.Package.Version == "" { sb.Package.Version = version d.SearchedBy = sb } case match.EcosystemParameters: if sb.Package.Version == "" { sb.Package.Version = version d.SearchedBy = sb } case match.CPEParameters: if sb.Package.Version == "" { sb.Package.Version = version d.SearchedBy = sb } } } } } } // ownedFilesFor returns the files owned by the package if its metadata implements [pkg.FileOwner]. func ownedFilesFor(p pkg.Package) []string { if fo, ok := p.Metadata.(pkg.FileOwner); ok { return fo.OwnedFiles() } return nil } ================================================ FILE: grype/matcher/internal/distro_test.go ================================================ package internal import ( "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" syftPkg "github.com/anchore/syft/syft/pkg" ) func newMockProviderByDistro() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { // direct... PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.5-6", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "secdb:distro:debian:8", }, }, { PackageName: "sles_test_package", Constraint: version.MustGetConstraint("< 2014.1.5-6", version.RpmFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-4", Namespace: "secdb:distro:sles:12.5", }, }, }...) } func TestFindMatchesByPackageDistro(t *testing.T) { p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "2014.1.3-6", Type: syftPkg.DebPkg, Upstreams: []pkg.UpstreamPackage{ { Name: "neutron-devel", }, }, } d := distro.New(distro.Debian, "8", "") p.Distro = d expected := []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "debian", Version: "8", }, Package: match.PackageParameter{ Name: "neutron", Version: "2014.1.3-6", }, Namespace: "secdb:distro:debian:8", }, Found: match.DistroResult{ VersionConstraint: "< 2014.1.5-6 (deb)", VulnerabilityID: "CVE-2014-fake-1", }, Matcher: match.PythonMatcher, }, }, }, } store := newMockProviderByDistro() actual, ignored, err := MatchPackageByDistro(store, p, nil, match.PythonMatcher, nil) require.NoError(t, err) require.Empty(t, ignored) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) // prove we do not search for unknown versions p.Version = "unknown" actual, ignored, err = MatchPackageByDistro(store, p, nil, match.PythonMatcher, nil) require.NoError(t, err) require.Empty(t, ignored) assert.Empty(t, actual) } func TestMatchPackageByDistroWithIgnoreRules(t *testing.T) { ownedFiles := pkg.ApkMetadata{Files: []pkg.ApkFileRecord{ {Path: "/usr/lib/python3/dist-packages/requests"}, {Path: "/usr/bin/python3"}, }} tests := []struct { name string pkg pkg.Package vulnerabilities []vulnerability.Vulnerability expectedIgnoreVulnIDs []string expectedMatchIDs []string expectNoIgnoreRules bool }{ { name: "package version is already fixed - should produce ignore rules scoped to paths", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "2.25.1-14.el8", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-backported", Namespace: "secdb:distro:redhat:8"}, }, }, // one rule per (vulnID, path) pair expectedIgnoreVulnIDs: []string{"CVE-2023-backported", "CVE-2023-backported"}, }, { name: "package version is still vulnerable - should NOT produce ignore rules", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "2.25.1-10.el8", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-backported", Namespace: "secdb:distro:redhat:8"}, }, }, expectedMatchIDs: []string{"CVE-2023-backported"}, expectNoIgnoreRules: true, }, { name: "distro has no data about the package - should NOT produce ignore rules (search miss)", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-something-obscure", Version: "1.0.0-1.el8", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ // no vulnerabilities for this package in the distro feed { PackageName: "other-package", Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-other", Namespace: "secdb:distro:redhat:8"}, }, }, expectNoIgnoreRules: true, }, { name: "mix of fixed and still-vulnerable CVEs - should only produce ignore rules for fixed ones", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "2.25.1-14.el8", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { // fixed: package version 2.25.1-14.el8 >= fix version PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-already-fixed", Namespace: "secdb:distro:redhat:8"}, }, { // still vulnerable: package version 2.25.1-14.el8 < 2.25.1-20.el8 PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-20.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-still-vulnerable", Namespace: "secdb:distro:redhat:8"}, }, }, expectedMatchIDs: []string{"CVE-2023-still-vulnerable"}, // one rule per path for the fixed CVE only expectedIgnoreVulnIDs: []string{"CVE-2023-already-fixed", "CVE-2023-already-fixed"}, }, { name: "fixed CVE with related vulnerabilities - should produce ignore rules for all IDs at all paths", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "2.25.1-14.el8", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-backported", Namespace: "secdb:distro:redhat:8"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "GHSA-xxxx-yyyy-zzzz", Namespace: "github:language:python"}, }, }, }, // both IDs × 2 paths = 4 rules expectedIgnoreVulnIDs: []string{"CVE-2023-backported", "CVE-2023-backported", "GHSA-xxxx-yyyy-zzzz", "GHSA-xxxx-yyyy-zzzz"}, }, { name: "no distro on package - should NOT produce ignore rules", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "2.25.1-14.el8", Type: syftPkg.RpmPkg, Distro: nil, Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-backported", Namespace: "secdb:distro:redhat:8"}, }, }, expectNoIgnoreRules: true, }, { name: "unknown version - should NOT produce ignore rules", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "unknown", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), Metadata: ownedFiles, }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-backported", Namespace: "secdb:distro:redhat:8"}, }, }, expectNoIgnoreRules: true, }, { name: "no FileOwner metadata - should NOT produce ignore rules even if fixed", pkg: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "python3-requests", Version: "2.25.1-14.el8", Type: syftPkg.RpmPkg, Distro: distro.New(distro.RedHat, "8", ""), // no Metadata — does not implement FileOwner }, vulnerabilities: []vulnerability.Vulnerability{ { PackageName: "python3-requests", Constraint: version.MustGetConstraint("< 2.25.1-14.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2023-backported", Namespace: "secdb:distro:redhat:8"}, }, }, expectNoIgnoreRules: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { store := mock.VulnerabilityProvider(test.vulnerabilities...) matches, ignoreFilters, err := MatchPackageByDistroWithOwnedFiles(store, test.pkg, nil, match.PythonMatcher, nil) require.NoError(t, err) // verify matches var gotMatchIDs []string for _, m := range matches { gotMatchIDs = append(gotMatchIDs, m.Vulnerability.ID) } if len(test.expectedMatchIDs) > 0 { assert.ElementsMatch(t, test.expectedMatchIDs, gotMatchIDs, "unexpected match IDs") } if test.expectNoIgnoreRules { assert.Empty(t, ignoreFilters, "expected no ignore rules") return } // extract the vulnerability IDs from the ignore rules var gotVulnIDs []string for _, filter := range ignoreFilters { rule, ok := filter.(match.IgnoreRule) require.True(t, ok, "expected IgnoreRule type") gotVulnIDs = append(gotVulnIDs, rule.Vulnerability) assert.True(t, rule.IncludeAliases, "expected IncludeAliases to be true") assert.Equal(t, "DistroPackageFixed", rule.Reason) assert.NotEmpty(t, rule.Package.Location, "expected location to be set") } assert.ElementsMatch(t, test.expectedIgnoreVulnIDs, gotVulnIDs, "unexpected ignore rule vulnerability IDs") }) } } func TestFindMatchesByPackageDistroSles(t *testing.T) { p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "sles_test_package", Version: "2014.1.3-6", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ { Name: "sles_test_package", }, }, } d := distro.New(distro.SLES, "12.5", "") p.Distro = d expected := []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2014-fake-4", }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "sles", Version: "12.5", }, Package: match.PackageParameter{ Name: "sles_test_package", Version: "2014.1.3-6", }, Namespace: "secdb:distro:sles:12.5", }, Found: match.DistroResult{ VersionConstraint: "< 2014.1.5-6 (rpm)", VulnerabilityID: "CVE-2014-fake-4", }, Matcher: match.PythonMatcher, }, }, }, } store := newMockProviderByDistro() actual, ignored, err := MatchPackageByDistro(store, p, nil, match.PythonMatcher, nil) assert.NoError(t, err) require.Empty(t, ignored) assertMatchesUsingIDsForVulnerabilities(t, expected, actual) } ================================================ FILE: grype/matcher/internal/eol.go ================================================ package internal import ( "time" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) // EOLStatus represents the end-of-life status of a distro type EOLStatus struct { IsEOL bool // true if the distro is past its EOL date IsEOAS bool // true if the distro is past its EOAS date EOLDate *time.Time // the EOL date, if known EOASDate *time.Time // the EOAS date, if known } // CheckDistroEOL checks if the given distro is past its end-of-life date. // Returns EOLStatus with the status and dates. If the provider doesn't support // EOL checking or the distro has no EOL data, returns a zero EOLStatus. func CheckDistroEOL(provider vulnerability.Provider, d *distro.Distro) EOLStatus { if d == nil { return EOLStatus{} } checker, ok := provider.(vulnerability.EOLChecker) if !ok { log.Trace("vulnerability provider does not support EOL checking") return EOLStatus{} } eolDate, eoasDate, err := checker.GetOperatingSystemEOL(d) if err != nil { log.WithFields("distro", d.String(), "error", err).Debug("failed to get EOL status for distro") return EOLStatus{} } now := time.Now() status := EOLStatus{ EOLDate: eolDate, EOASDate: eoasDate, } if eolDate != nil && now.After(*eolDate) { status.IsEOL = true } if eoasDate != nil && now.After(*eoasDate) { status.IsEOAS = true } return status } // IsDistroEOL is a convenience function that returns true if the distro is past its EOL date. func IsDistroEOL(provider vulnerability.Provider, d *distro.Distro) bool { return CheckDistroEOL(provider, d).IsEOL } ================================================ FILE: grype/matcher/internal/eol_test.go ================================================ package internal import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" ) // mockEOLProvider wraps mock.VulnerabilityProvider and adds EOLChecker support type mockEOLProvider struct { vulnerability.Provider eolDate *time.Time eoasDate *time.Time err error } func (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) { return m.eolDate, m.eoasDate, m.err } func newMockEOLProvider(eolDate, eoasDate *time.Time) *mockEOLProvider { return &mockEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDate: eolDate, eoasDate: eoasDate, } } func TestCheckDistroEOL_NilDistro(t *testing.T) { provider := newMockEOLProvider(nil, nil) status := CheckDistroEOL(provider, nil) assert.False(t, status.IsEOL) assert.False(t, status.IsEOAS) assert.Nil(t, status.EOLDate) assert.Nil(t, status.EOASDate) } func TestCheckDistroEOL_ProviderDoesNotSupportEOL(t *testing.T) { // use base mock provider without EOLChecker provider := mock.VulnerabilityProvider() d := distro.New(distro.Ubuntu, "18.04", "") status := CheckDistroEOL(provider, d) assert.False(t, status.IsEOL) assert.False(t, status.IsEOAS) assert.Nil(t, status.EOLDate) assert.Nil(t, status.EOASDate) } func TestCheckDistroEOL_PastEOLDate(t *testing.T) { pastDate := time.Now().AddDate(-1, 0, 0) // 1 year ago provider := newMockEOLProvider(&pastDate, nil) d := distro.New(distro.Ubuntu, "18.04", "") status := CheckDistroEOL(provider, d) assert.True(t, status.IsEOL) assert.False(t, status.IsEOAS) assert.NotNil(t, status.EOLDate) assert.Equal(t, pastDate, *status.EOLDate) } func TestCheckDistroEOL_FutureEOLDate(t *testing.T) { futureDate := time.Now().AddDate(1, 0, 0) // 1 year from now provider := newMockEOLProvider(&futureDate, nil) d := distro.New(distro.Ubuntu, "22.04", "") status := CheckDistroEOL(provider, d) assert.False(t, status.IsEOL) assert.False(t, status.IsEOAS) assert.NotNil(t, status.EOLDate) assert.Equal(t, futureDate, *status.EOLDate) } func TestCheckDistroEOL_PastEOASDate(t *testing.T) { pastEOAS := time.Now().AddDate(-1, 0, 0) // 1 year ago futureEOL := time.Now().AddDate(1, 0, 0) // 1 year from now provider := newMockEOLProvider(&futureEOL, &pastEOAS) d := distro.New(distro.Ubuntu, "20.04", "") status := CheckDistroEOL(provider, d) assert.False(t, status.IsEOL) assert.True(t, status.IsEOAS) assert.NotNil(t, status.EOLDate) assert.NotNil(t, status.EOASDate) } func TestCheckDistroEOL_NoEOLData(t *testing.T) { provider := newMockEOLProvider(nil, nil) d := distro.New(distro.Ubuntu, "24.04", "") status := CheckDistroEOL(provider, d) assert.False(t, status.IsEOL) assert.False(t, status.IsEOAS) assert.Nil(t, status.EOLDate) assert.Nil(t, status.EOASDate) } func TestIsDistroEOL(t *testing.T) { tests := []struct { name string eolDate *time.Time expected bool }{ { name: "past EOL date returns true", eolDate: ptrTime(time.Now().AddDate(-1, 0, 0)), expected: true, }, { name: "future EOL date returns false", eolDate: ptrTime(time.Now().AddDate(1, 0, 0)), expected: false, }, { name: "nil EOL date returns false", eolDate: nil, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { provider := newMockEOLProvider(tt.eolDate, nil) d := distro.New(distro.Ubuntu, "18.04", "") result := IsDistroEOL(provider, d) assert.Equal(t, tt.expected, result) }) } } func ptrTime(t time.Time) *time.Time { return &t } ================================================ FILE: grype/matcher/internal/language.go ================================================ package internal import ( "fmt" "slices" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcherType match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match var ignored []match.IgnoreFilter for _, name := range store.PackageSearchNames(p) { nameMatches, nameIgnores, err := MatchPackageByEcosystemPackageName(store, p, name, matcherType) if err != nil { return nil, nil, err } matches = append(matches, nameMatches...) ignored = append(ignored, nameIgnores...) } return matches, ignored, nil } func MatchPackageByEcosystemPackageName(vp vulnerability.Provider, p pkg.Package, packageName string, matcherType match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) { if isUnknownVersion(p.Version) { log.WithFields("package", p.Name).Trace("skipping package with unknown version") return nil, nil, nil } provider := result.NewProvider(vp, p, matcherType) criteria := []vulnerability.Criteria{ search.ByEcosystem(p.Language, p.Type), search.ByPackageName(packageName), OnlyQualifiedPackages(p), OnlyVulnerableVersions(version.New(p.Version, pkg.VersionFormat(p))), OnlyNonWithdrawnVulnerabilities(), } // TODO: previous impl set confidence to 1, this results in // a confidence of zero. What should it be? disclosures, err := provider.FindResults(criteria...) if err != nil { return nil, nil, fmt.Errorf("matcher failed to fetch disclosure language=%q pkg=%q: %w", p.Language, p.Name, err) } // we want to perform the same results, but look for explicit naks, which indicates that a vulnerability should not apply criteria = append(criteria, search.ForUnaffected()) unaffected, err := provider.FindResults(criteria...) if err != nil { return nil, nil, fmt.Errorf("matcher failed to fetch resolution language=%q pkg=%q: %w", p.Language, p.Name, err) } // remove any disclosures that have been explicitly nacked remaining := disclosures.Remove(unaffected) return remaining.ToMatches(), constructIgnoreFilters(unaffected, p), err } func constructIgnoreFilters(unaffectedVulns result.Set, p pkg.Package) []match.IgnoreFilter { var ignores []match.IgnoreFilter // collect all IDs to exclude var ids []string for _, vulnResults := range unaffectedVulns { for _, vulnResult := range vulnResults { ids = append(ids, vulnResult.ID) for _, vuln := range vulnResult.Vulnerabilities { if !slices.Contains(ids, vuln.ID) { ids = append(ids, vuln.ID) } for _, id := range vuln.RelatedVulnerabilities { if !slices.Contains(ids, id.ID) { ids = append(ids, id.ID) } } } } } // ignore rules for all IDs for _, id := range ids { ignores = append(ignores, match.IgnoreRule{ Vulnerability: id, IncludeAliases: true, Reason: "UnaffectedPackageEntry", Package: match.IgnoreRulePackage{ Type: string(p.Type), Name: p.Name, Version: p.Version, }, }) } return ignores } ================================================ FILE: grype/matcher/internal/language_test.go ================================================ package internal import ( "slices" "sort" "strings" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func newMockProviderRuby() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-1", Namespace: "github:language:ruby", }, PackageName: "activerecord", // make sure we find it with semVer constraint Constraint: version.MustGetConstraint("< 3.7.6", version.GemFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-2", Namespace: "github:language:ruby", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat), }, { // ignore filter entry Reference: vulnerability.Reference{ ID: "CVE-2017-fake-2", Namespace: "github:language:ruby", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.GemFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-1", Namespace: "github:language:ruby", }, PackageName: "nokogiri", // make sure we find it with gem version constraint Constraint: version.MustGetConstraint("< 1.7.6", version.GemFormat), // detail a fix by vendor "foo" Fix: vulnerability.Fix{ Versions: []string{"1.7.4+foo.1"}, State: vulnerability.FixStateFixed, }, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-2", Namespace: "github:language:ruby", }, PackageName: "nokogiri", Constraint: version.MustGetConstraint("< 1.7.4", version.GemFormat), }, }...) } func expectedMatchRuby(p pkg.Package, constraint string) []match.Match { return []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2017-fake-1", }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{Name: p.Name, Version: p.Version}, }, Found: match.EcosystemResult{ VulnerabilityID: "CVE-2017-fake-1", VersionConstraint: constraint, }, Matcher: match.RubyGemMatcher, }, }, }, } } func TestFindMatchesByPackageRuby(t *testing.T) { cases := []struct { p pkg.Package constraint string expIgnores []match.IgnoreRule assertEmpty bool }{ { constraint: "< 3.7.6 (gem)", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "activerecord", Version: "3.7.5", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, }, { constraint: "< 1.7.6 (gem)", // no ignores expected as version doesn't contain +foo p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "nokogiri", Version: "1.7.5", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, }, { p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "nokogiri", Version: "unknown", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, }, assertEmpty: true, }, } store := newMockProviderRuby() for _, c := range cases { t.Run(c.p.Name, func(t *testing.T) { actual, ignored, err := MatchPackageByLanguage(store, c.p, match.RubyGemMatcher) require.NoError(t, err) assert.ElementsMatch(t, ignored, c.expIgnores) if c.assertEmpty { assert.Empty(t, actual) return } assertMatchesUsingIDsForVulnerabilities(t, expectedMatchRuby(c.p, c.constraint), actual) }) } } // Golang tests func expectedMatchGolang(p pkg.Package, vulnConstraint map[string]string) []match.Match { matches := make([]match.Match, 0, len(vulnConstraint)) // get sorted keys for consistent test results keys := make([]string, 0, len(vulnConstraint)) for k := range vulnConstraint { keys = append(keys, k) } sort.Strings(keys) for _, vuln := range keys { constraint := vulnConstraint[vuln] matches = append(matches, match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: vuln, }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, // Confidence zero since ecosystems get hardcoded confidence of zero Confidence: 1, SearchedBy: match.EcosystemParameters{ Language: "go", Namespace: "github:language:go", Package: match.PackageParameter{Name: p.Name, Version: p.Version}, }, Found: match.EcosystemResult{ VulnerabilityID: vuln, VersionConstraint: constraint, }, Matcher: match.GoModuleMatcher, }, }, }) } return matches } func newMockProviderGolang() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-1", Namespace: "github:language:go", }, PackageName: "package", Constraint: version.MustGetConstraint("< 1.2.4", version.GolangFormat), Fix: vulnerability.Fix{ Versions: []string{"1.2.4"}, State: vulnerability.FixStateFixed, }, }, { Reference: vulnerability.Reference{ ID: "CVE-2017-fake-2", Namespace: "github:language:go", }, PackageName: "package", Constraint: version.MustGetConstraint("< 1.3.1", version.GolangFormat), Fix: vulnerability.Fix{ Versions: []string{"1.3.1"}, State: vulnerability.FixStateFixed, }, }, { // unaffected entry Reference: vulnerability.Reference{ ID: "CVE-2017-fake-1", Namespace: "github:language:go", }, PackageName: "package", Constraint: version.MustGetConstraint("= 1.2.1+foo.1", version.GolangFormat), Fix: vulnerability.Fix{ Versions: []string{"1.2.1+foo.1"}, State: vulnerability.FixStateFixed, }, Unaffected: true, }, }...) } func TestFindMatchesByPackageGolang(t *testing.T) { cases := []struct { p pkg.Package expMatches map[string]string unaffected bool }{ { p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package", Version: "1.2.3", Language: syftPkg.Go, Type: syftPkg.GoModulePkg, }, expMatches: map[string]string{"CVE-2017-fake-2": "< 1.3.1 (go)", "CVE-2017-fake-1": "< 1.2.4 (go)"}, }, { p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package", Version: "1.2.5", Language: syftPkg.Go, Type: syftPkg.GoModulePkg, }, expMatches: map[string]string{"CVE-2017-fake-2": "< 1.3.1 (go)"}, }, { p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "package", Version: "1.2.1+foo.1", Language: syftPkg.Go, Type: syftPkg.GoModulePkg, }, expMatches: map[string]string{"CVE-2017-fake-2": "< 1.3.1 (go)"}, unaffected: true, // this vuln matches an unaffected entry }, } store := newMockProviderGolang() for _, c := range cases { t.Run(c.p.Name, func(t *testing.T) { actual, ignored, err := MatchPackageByLanguage(store, c.p, match.GoModuleMatcher) // sort for consistency slices.SortFunc(actual, func(a, b match.Match) int { return strings.Compare(a.Vulnerability.ID, b.Vulnerability.ID) }) require.NoError(t, err) if c.unaffected { assert.NotEmpty(t, ignored) } else { assert.Empty(t, ignored) } assertMatchesUsingIDsForVulnerabilities(t, expectedMatchGolang(c.p, c.expMatches), actual) }) } } func Test_unaffectedPackageIgnoreRules(t *testing.T) { someProjectCPE := cpe.Must(`cpe:2.3:a:some_vendor:some_project:*:*:*:*:*:*:*:*`, cpe.DeclaredSource) provider := mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln1", Namespace: "github:language:python"}, Constraint: version.MustGetConstraint("< 1.2.3", version.PythonFormat), PackageName: "some_project", Unaffected: false, }, { Reference: vulnerability.Reference{ID: "vuln2", Namespace: "github:language:python"}, Constraint: version.MustGetConstraint("< 1.2.3", version.PythonFormat), PackageName: "some_project", Unaffected: true, }, { Reference: vulnerability.Reference{ID: "vuln2", Namespace: "nvd:cpe"}, Constraint: version.MustGetConstraint("< 1.2.3", version.PythonFormat), PackageName: "some_project", CPEs: []cpe.CPE{someProjectCPE}, Unaffected: false, }, }...) tests := []struct { name string pkg pkg.Package expected []match.IgnoreFilter }{ { name: "matching unaffected", pkg: pkg.Package{ Name: "some_project", Version: "1.2.2", Language: syftPkg.Python, Type: syftPkg.PythonPkg, CPEs: []cpe.CPE{someProjectCPE}, }, expected: []match.IgnoreFilter{ match.IgnoreRule{ Vulnerability: "vuln2", IncludeAliases: true, Reason: "UnaffectedPackageEntry", Package: match.IgnoreRulePackage{ Name: "some_project", Version: "1.2.2", Type: string(syftPkg.PythonPkg), }, }, }, }, { name: "not unaffected by version", pkg: pkg.Package{ Name: "some_project", Version: "1.2.4", Language: syftPkg.Python, Type: syftPkg.PythonPkg, CPEs: []cpe.CPE{someProjectCPE}, }, expected: nil, }, { name: "not unaffected by name", pkg: pkg.Package{ Name: "some_other_project", Version: "1.2.2", Language: syftPkg.Python, Type: syftPkg.PythonPkg, CPEs: []cpe.CPE{someProjectCPE}, }, expected: nil, }, { name: "not unaffected by type", pkg: pkg.Package{ Name: "some_project", Version: "1.2.2", Language: syftPkg.Go, Type: syftPkg.GoModulePkg, CPEs: []cpe.CPE{someProjectCPE}, }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, ignoreRules, err := MatchPackageByEcosystemPackageName(provider, tt.pkg, tt.pkg.Name, "") require.NoError(t, err) assert.Equal(t, tt.expected, ignoreRules) }) } } ================================================ FILE: grype/matcher/internal/only_non_withdrawn_vulnerabilities.go ================================================ package internal import ( "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" ) // OnlyNonWithdrawnVulnerabilities returns a criteria object that tests affected vulnerability is not withdrawn/rejected func OnlyNonWithdrawnVulnerabilities() vulnerability.Criteria { return search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) { // we should be using enumerations from all supported schema versions, but constants should not be imported here isWithdrawn := v.Status == "withdrawn" || v.Status == "rejected" if isWithdrawn { return false, "vulnerability is withdrawn or rejected", nil } return true, "", nil }) } ================================================ FILE: grype/matcher/internal/only_qualified_packages.go ================================================ package internal import ( "fmt" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" ) // OnlyQualifiedPackages returns a criteria object that tests vulnerability qualifiers against the provided package func OnlyQualifiedPackages(p pkg.Package) vulnerability.Criteria { return search.ByFunc(func(vuln vulnerability.Vulnerability) (bool, string, error) { for _, qualifier := range vuln.PackageQualifiers { satisfied, err := qualifier.Satisfied(p) if err != nil { return satisfied, fmt.Sprintf("unable to evaluate qualifier: %s", err.Error()), err } if !satisfied { // TODO: qualifiers don't have a good string representation return false, fmt.Sprintf("package does not satisfy qualifier: %#v", qualifier), nil } } return true, "", nil // all qualifiers passed }) } ================================================ FILE: grype/matcher/internal/only_vulnerable_targets.go ================================================ package internal import ( "fmt" "strings" "github.com/facebookincubator/nvdtools/wfn" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" syftCPE "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) // OnlyVulnerableTargets returns a criteria object that tests vulnerability qualifiers against the package vulnerability rules. // TODO: in the future this should be moved to underneath the store to avoid the need to recompute CPE comparisons and to leverage ecosystem aliases for target software func OnlyVulnerableTargets(p pkg.Package) vulnerability.Criteria { return search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) { matches, reasons := isVulnerableTarget(p, v) return matches, reasons, nil }) } // Determines if a vulnerability is an accurate match using the vulnerability's cpes' target software func isVulnerableTarget(p pkg.Package, vuln vulnerability.Vulnerability) (bool, string) { // Exclude OS package types from this logic, since they could be embedding any type of ecosystem package if isOSPackage(p) { return true, "" } packageTargetSwSet, vulnTargetSwSet := matchTargetSoftware(p.CPEs, vuln.CPEs) if len(vuln.CPEs) > 0 && packageTargetSwSet.IsEmpty() { reason := fmt.Sprintf("vulnerability target software(s) (%q) do not align with %s", strings.Join(vulnTargetSwSet.List(), ", "), packageElements(p, packageTargetSwSet.List())) return false, reason } // only strictly use CPE attributes to filter binary and unknown package types if p.Type == syftPkg.BinaryPkg || p.Type == syftPkg.UnknownPkg || p.Type == "" { if hasIntersectingTargetSoftware(packageTargetSwSet, vulnTargetSwSet) { // we have at least one target software in common return true, "" } // the package has a * target software, so should match with anything that's on the CPE. // note that this is two way (either the package has a * or the vuln has a * target software). if packageTargetSwSet.Has(wfn.Any) || vulnTargetSwSet.Has(wfn.Any) { return true, "" } reason := fmt.Sprintf("vulnerability target software(s) (%q) do not align with %s", strings.Join(vulnTargetSwSet.List(), ", "), packageElements(p, packageTargetSwSet.List())) return false, reason } // There are quite a few cases within java where other ecosystem components (particularly javascript packages) // are embedded directly within jar files, so we can't yet make this assumption with java as it will cause dropping // of valid vulnerabilities that syft has specific logic https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go#L48-L75 // to ensure will be surfaced if p.Language == syftPkg.Java { return true, "" } // if there are no CPEs then we can't make a decision if len(vuln.CPEs) == 0 { return true, "" } if hasIntersectingTargetSoftware(packageTargetSwSet, vulnTargetSwSet) { // we have at least one target software in common return true, "" } return refuteTargetSoftwareByPackageAttributes(p, vuln, packageTargetSwSet) } func refuteTargetSoftwareByPackageAttributes(p pkg.Package, vuln vulnerability.Vulnerability, packageTargetSwSet *strset.Set) (bool, string) { // this is purely based on package attributes and does not consider any package CPE target softwares (which the store already considers) var mismatchedTargetSoftware []string for _, c := range vuln.CPEs { targetSW := c.Attributes.TargetSW mismatchWithUnknownLanguage := syftPkg.LanguageByName(targetSW) != p.Language && isUnknownTarget(targetSW) unspecifiedTargetSW := targetSW == wfn.Any || targetSW == wfn.NA matchesByLanguage := syftPkg.LanguageByName(targetSW) == p.Language matchesByPackageType := syftCPE.TargetSoftwareToPackageType(targetSW) == p.Type if unspecifiedTargetSW || matchesByLanguage || matchesByPackageType || mismatchWithUnknownLanguage { return true, "" } mismatchedTargetSoftware = append(mismatchedTargetSoftware, targetSW) } reason := fmt.Sprintf("vulnerability target software(s) (%q) do not align with %s", strings.Join(mismatchedTargetSoftware, ", "), packageElements(p, packageTargetSwSet.List())) return false, reason } func isOSPackage(p pkg.Package) bool { return p.Type == syftPkg.AlpmPkg || p.Type == syftPkg.ApkPkg || p.Type == syftPkg.DebPkg || p.Type == syftPkg.KbPkg || p.Type == syftPkg.PortagePkg || p.Type == syftPkg.RpmPkg } func isUnknownTarget(targetSW string) bool { if syftPkg.LanguageByName(targetSW) != syftPkg.UnknownLanguage { return false } // There are some common target software CPE components which are not currently // supported by syft but are significant sources of false positives and should be // considered known for the purposes of filtering here known := map[string]bool{ "joomla": true, "joomla\\!": true, "drupal": true, } if _, ok := known[targetSW]; ok { return false } return true } func matchTargetSoftware(pkgCPEs []cpe.CPE, vulnCPEs []cpe.CPE) (*strset.Set, *strset.Set) { pkgTsw := strset.New() vulnTsw := strset.New() for _, c := range vulnCPEs { for _, p := range pkgCPEs { if matchesAttributesExceptVersionAndTSW(c.Attributes, p.Attributes) { // include any value including empty string (which means ANY value) pkgTsw.Add(p.Attributes.TargetSW) vulnTsw.Add(c.Attributes.TargetSW) } } } return pkgTsw, vulnTsw } func matchesAttributesExceptVersionAndTSW(a1 cpe.Attributes, a2 cpe.Attributes) bool { // skip version, update, and target software if !matchesAttribute(a1.Product, a2.Product) || !matchesAttribute(a1.Vendor, a2.Vendor) || !matchesAttribute(a1.Part, a2.Part) || !matchesAttribute(a1.Language, a2.Language) || !matchesAttribute(a1.SWEdition, a2.SWEdition) || !matchesAttribute(a1.TargetHW, a2.TargetHW) || !matchesAttribute(a1.Other, a2.Other) || !matchesAttribute(a1.Edition, a2.Edition) { return false } return true } func matchesAttribute(a1, a2 string) bool { return a1 == "" || a2 == "" || strings.EqualFold(a1, a2) } func hasIntersectingTargetSoftware(set1, set2 *strset.Set) bool { set1Pkg := normalizeTargetSoftwares(set1.List()) set2Pkg := normalizeTargetSoftwares(set2.List()) intersection := strset.Intersection(set1Pkg, set2Pkg) return !intersection.IsEmpty() } func normalizeTargetSoftwares(ts []string) *strset.Set { normalizedTargetSWs := strset.New() for _, ts := range ts { // Attempt to normalize target sw to package type, e.g. node and nodejs should match pt := string(syftCPE.TargetSoftwareToPackageType(ts)) if pt == "" && ts != "*" && ts != "?" && ts != "-" { // normalizing failed; preserve raw cpe target sw string as the type // unless it is wildcard pt = strings.ToLower(ts) } if pt != "" { normalizedTargetSWs.Add(pt) } } return normalizedTargetSWs } func packageElements(p pkg.Package, ts []string) string { nameVersion := fmt.Sprintf("%s@%s", p.Name, p.Version) pType := string(p.Type) if pType == "" { pType = "?" } pLanguage := string(p.Language) if pLanguage == "" { pLanguage = "?" } targetSW := strings.Join(ts, ",") if (len(ts) == 0) || (len(ts) == 1 && ts[0] == wfn.Any) { targetSW = "*" } return fmt.Sprintf("pkg(%s type=%q language=%q targets=%q)", nameVersion, pType, pLanguage, targetSW) } ================================================ FILE: grype/matcher/internal/only_vulnerable_targets_test.go ================================================ package internal import ( "testing" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestIsVulnerableTarget(t *testing.T) { tests := []struct { name string pkg pkg.Package vuln vulnerability.Vulnerability expectedMatches bool expectedReason string }{ { name: "OS package should always match", pkg: pkg.Package{ Name: "openssl", Version: "1.1.1k", Type: syftPkg.RpmPkg, Language: syftPkg.UnknownLanguage, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-3449", Namespace: "nvd:cpe", }, PackageName: "openssl", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*", ""), }, }, expectedMatches: true, }, { name: "binary package should always match", pkg: pkg.Package{ Name: "bash", Version: "5.0.17", Type: syftPkg.BinaryPkg, Language: syftPkg.UnknownLanguage, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:gnu:bash:5.0.17:*:*:*:*:*:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-12345", Namespace: "nvd:cpe", }, PackageName: "bash", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:gnu:bash:5.0.17:*:*:*:*:*:*:*", ""), }, }, expectedMatches: true, }, { name: "unknown package should always match", pkg: pkg.Package{ Name: "unknown-pkg", Version: "1.0.0", Type: syftPkg.UnknownPkg, Language: syftPkg.UnknownLanguage, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:unknown:unknown-pkg:1.0.0:*:*:*:*:*:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-98765", Namespace: "nvd:cpe", }, PackageName: "unknown-pkg", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:unknown:unknown-pkg:1.0.0:*:*:*:*:*:*:*", ""), }, }, expectedMatches: true, }, { name: "java package should always match", pkg: pkg.Package{ Name: "log4j-core", Version: "2.14.1", Type: syftPkg.JavaPkg, Language: syftPkg.Java, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-44228", Namespace: "nvd:cpe", }, PackageName: "log4j-core", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""), }, }, expectedMatches: true, }, { name: "package with no CPEs should fail", pkg: pkg.Package{ Name: "example-lib", Version: "1.0.0", Type: syftPkg.NpmPkg, Language: syftPkg.JavaScript, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-87654", Namespace: "nvd:cpe", }, PackageName: "example-lib", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example-lib:1.0.0:*:*:*:*:*:*:*", ""), }, }, expectedMatches: false, expectedReason: `vulnerability target software(s) ("") do not align with pkg(example-lib@1.0.0 type="npm" language="javascript" targets="*")`, }, { name: "vulnerability with no CPEs should match", pkg: pkg.Package{ Name: "example-lib", Version: "1.0.0", Type: syftPkg.NpmPkg, Language: syftPkg.JavaScript, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:example:example-lib:1.0.0:*:*:*:*:*:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-87654", Namespace: "nvd:cpe", }, PackageName: "example-lib", }, expectedMatches: true, }, { name: "package with wildcard targetSW should match", pkg: pkg.Package{ Name: "react", Version: "17.0.2", Type: syftPkg.NpmPkg, Language: syftPkg.JavaScript, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:facebook:react:17.0.2:*:*:*:*:*:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-12345", Namespace: "nvd:cpe", }, PackageName: "react", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:facebook:react:17.0.2:*:*:*:*:node.js:*:*", ""), }, }, expectedMatches: true, }, { name: "intersecting target software should match", pkg: pkg.Package{ Name: "lodash", Version: "4.17.20", Type: syftPkg.NpmPkg, Language: syftPkg.JavaScript, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-23337", Namespace: "nvd:cpe", }, PackageName: "lodash", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*", ""), }, }, expectedMatches: true, }, { name: "non-intersecting target software with matching language should match", pkg: pkg.Package{ Name: "express", Version: "4.17.1", Type: syftPkg.RpmPkg, // important! Language: syftPkg.JavaScript, // we're using this to match against the vuln TSW CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:expressjs:express:4.17.1:*:*:*:*:react:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2022-24999", Namespace: "nvd:cpe", }, PackageName: "express", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:expressjs:express:4.17.1:*:*:*:*:node.js:*:*", ""), }, }, expectedMatches: true, }, { name: "non-intersecting target software with matching package type should fail", pkg: pkg.Package{ Name: "moment", Version: "2.29.1", Type: syftPkg.NpmPkg, // we're using this to match against the vuln TSW Language: syftPkg.CPP, // important! CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:moment:moment:2.29.1:*:*:*:*:doesntmatter:*:*", ""), }, }, vuln: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2022-31129", Namespace: "nvd:cpe", }, PackageName: "moment", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:moment:moment:2.29.1:*:*:*:*:node.js:*:*", ""), }, }, expectedMatches: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { matches, reason := isVulnerableTarget(test.pkg, test.vuln) assert.Equal(t, test.expectedMatches, matches, "matches result should be as expected") assert.Equal(t, test.expectedReason, reason, "reason should match expected") }) } } func Test_isUnknownTarget(t *testing.T) { tests := []struct { name string targetSW string expected bool }{ {name: "supported syft language", targetSW: "python", expected: false}, {name: "supported non-syft language CPE component", targetSW: "joomla", expected: false}, {name: "unknown component", targetSW: "abc", expected: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { u := isUnknownTarget(test.targetSW) assert.Equal(t, test.expected, u) }) } } func TestPkgTypesFromTargetSoftware(t *testing.T) { tests := []struct { name string input []string expected []string }{ { name: "empty input", input: []string{}, expected: []string{}, }, { name: "single input with known mapping", input: []string{"node.js"}, expected: []string{string(syftPkg.NpmPkg)}, }, { name: "multiple inputs with known mappings", input: []string{"python", "ruby", "java"}, expected: []string{string(syftPkg.PythonPkg), string(syftPkg.GemPkg), string(syftPkg.JavaPkg)}, }, { name: "case insensitive input", input: []string{"Python", "RUBY", "Java"}, expected: []string{string(syftPkg.PythonPkg), string(syftPkg.GemPkg), string(syftPkg.JavaPkg)}, }, { name: "mixed known and unknown inputs", input: []string{"python", "unknown", "ruby"}, expected: []string{string(syftPkg.PythonPkg), "unknown", string(syftPkg.GemPkg)}, }, { name: "all unknown inputs", input: []string{"unknown1", "unknown2", "unknown3"}, expected: []string{"unknown1", "unknown2", "unknown3"}, }, { name: "inputs with spaces and hyphens", input: []string{"redhat-enterprise-linux", "jenkins ci"}, expected: []string{string(syftPkg.RpmPkg), string(syftPkg.JavaPkg)}, }, { name: "aliases for the same package type", input: []string{"nodejs", "npm", "javascript"}, expected: []string{string(syftPkg.NpmPkg)}, }, { name: "wildcards and special characters should be ignored", input: []string{"*", "?", "-", ""}, expected: []string{}, }, { name: "Linux distributions", input: []string{"alpine", "debian", "redhat", "gentoo"}, expected: []string{string(syftPkg.ApkPkg), string(syftPkg.DebPkg), string(syftPkg.RpmPkg), string(syftPkg.PortagePkg)}, }, { name: ".NET ecosystem", input: []string{".net", "asp.net", "c#"}, expected: []string{string(syftPkg.DotnetPkg)}, }, { name: "JavaScript ecosystem", input: []string{"javascript", "node.js", "jquery"}, expected: []string{string(syftPkg.NpmPkg)}, }, { name: "Java ecosystem", input: []string{"java", "maven", "kafka", "log4j"}, expected: []string{string(syftPkg.JavaPkg)}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := normalizeTargetSoftwares(test.input) assert.ElementsMatch(t, test.expected, actual.List(), "package types should match") }) } } func TestHasIntersectingTargetSoftware(t *testing.T) { tests := []struct { name string set1 []string set2 []string expected bool }{ // basic assertions around sets normalized to package types { name: "empty sets", set1: []string{}, set2: []string{}, expected: false, }, { name: "first set empty", set1: []string{}, set2: []string{"nodejs", "python"}, expected: false, }, { name: "second set empty", set1: []string{"java", "ruby"}, set2: []string{}, expected: false, }, { name: "intersecting sets - direct match", set1: []string{"nodejs", "python"}, set2: []string{"nodejs", "ruby"}, expected: true, }, { name: "intersecting sets - aliases", set1: []string{"node.js"}, set2: []string{"npm"}, expected: true, }, { name: "non-intersecting sets", set1: []string{"python", "ruby"}, set2: []string{"java", "golang"}, expected: false, }, { name: "multiple intersections", set1: []string{"python", "ruby", "nodejs"}, set2: []string{"javascript", "python", "java"}, expected: true, }, { name: "case insensitive", set1: []string{"Python", "Ruby"}, set2: []string{"python", "java"}, expected: true, }, { name: "wildcard in first set", set1: []string{"*"}, set2: []string{"nodejs", "python"}, expected: false, // * doesn't map to a package type }, { name: "special linux distro aliases", set1: []string{"rhel", "opensuse"}, set2: []string{"redhat"}, expected: true, }, { name: "different terminology for same ecosystem", set1: []string{"c#"}, set2: []string{"dotnet"}, expected: true, }, { name: "spaces and hyphens handling", set1: []string{"jenkins ci"}, set2: []string{"jenkins-ci"}, expected: true, }, // ecosystem specific cases { name: "npm package vs node.js vulnerability", set1: []string{"npm"}, set2: []string{"node.js"}, expected: true, }, { name: "python package vs django vulnerability", set1: []string{"python"}, set2: []string{"django"}, expected: false, // django is not mapped to a package type in the current implementation }, { name: "java package vs multiple java ecosystem vulnerabilities", set1: []string{"java"}, set2: []string{"tomcat", "log4j", "maven"}, expected: true, }, { name: "linux distributions match with different aliases", set1: []string{"redhat"}, set2: []string{"centos", "fedora", "rhel"}, expected: true, }, { name: "no common package types", set1: []string{"python", "ruby"}, set2: []string{"nodejs", "php"}, expected: false, }, { name: "mixed case and formatting", set1: []string{"Node.js", "Ruby-On-Rails"}, set2: []string{"javascript", "gem"}, expected: true, }, { name: ".NET ecosystem different terms", set1: []string{".net-framework"}, set2: []string{"c#", "nuget"}, expected: true, }, { name: "WordPress ecosystem", set1: []string{"wordpress"}, set2: []string{"wordpress_plugin"}, expected: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { set1 := strset.New(test.set1...) set2 := strset.New(test.set2...) actual := hasIntersectingTargetSoftware(set1, set2) assert.Equal(t, test.expected, actual, "integrated target software intersection should match expected") }) } } ================================================ FILE: grype/matcher/internal/only_vulnerable_versions.go ================================================ package internal import ( "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" ) // OnlyVulnerableVersions returns a criteria object that tests affected vulnerability ranges against the provided version func OnlyVulnerableVersions(v *version.Version) vulnerability.Criteria { if v == nil || v.Raw == "" { // if no version is provided, match everything return search.ByFunc(func(_ vulnerability.Vulnerability) (bool, string, error) { return true, "", nil }) // since we return true the summary is not used } return search.ByVersion(*v) } ================================================ FILE: grype/matcher/internal/result/match_details_set.go ================================================ package result import "github.com/anchore/grype/grype/match" type MatchDetailsSet struct { order []match.Detail seen map[match.Detail]struct{} } func NewMatchDetailsSet(ds ...match.Detail) MatchDetailsSet { s := MatchDetailsSet{ order: []match.Detail{}, seen: make(map[match.Detail]struct{}), } for _, detail := range ds { s.Add(detail) } return s } func (ds *MatchDetailsSet) Add(detail match.Detail) { if _, exists := ds.seen[detail]; !exists { ds.order = append(ds.order, detail) ds.seen[detail] = struct{}{} } } func (ds MatchDetailsSet) ToSlice() []match.Detail { return ds.order } ================================================ FILE: grype/matcher/internal/result/provider.go ================================================ package result import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" ) var _ Provider = (*provider)(nil) type Provider interface { FindResults(criteria ...vulnerability.Criteria) (Set, error) } type provider struct { vulnProvider vulnerability.Provider catalogedPkg pkg.Package // this is what is passed into the matcher matcher match.MatcherType } func NewProvider(vp vulnerability.Provider, catalogedPkg pkg.Package, matcher match.MatcherType) Provider { return provider{ vulnProvider: vp, catalogedPkg: catalogedPkg, matcher: matcher, } } func (p provider) FindResults(criteria ...vulnerability.Criteria) (Set, error) { results := Set{} // get each iteration here so detailProvider will have the specific values used for searches for _, cs := range search.CriteriaIterator(criteria) { vulns, err := p.vulnProvider.FindVulnerabilities(cs...) if err != nil { return Set{}, err } for _, v := range vulns { if v.ID == "" { continue // skip vulnerabilities without an ID (should never happen) } newResult := Result{ ID: v.ID, Vulnerabilities: []vulnerability.Vulnerability{v}, Details: detailProvider(p.matcher, p.catalogedPkg, criteria, v), Package: &p.catalogedPkg, } results[v.ID] = append(results[v.ID], newResult) } } return results, nil } func detailProvider(matcher match.MatcherType, catalogedPkg pkg.Package, criteriaSet []vulnerability.Criteria, vuln vulnerability.Vulnerability) match.Details { cpeParams, distroParams, ecosystemParams, pkgParams := extractSearchParameters(criteriaSet, vuln) distroMatchType := determineMatchType(catalogedPkg, pkgParams) applyPackageParamsToSearchParams(pkgParams, &cpeParams, &distroParams, &ecosystemParams) constraintStr := getConstraintString(vuln) return buildMatchDetails(matcher, distroMatchType, constraintStr, vuln, cpeParams, distroParams, ecosystemParams) } // extractSearchParameters processes criteria set and extracts search parameters for different match types func extractSearchParameters(criteriaSet []vulnerability.Criteria, vuln vulnerability.Vulnerability) ([]match.CPEParameters, []match.DistroParameters, []match.EcosystemParameters, *match.PackageParameter) { var cpeParams []match.CPEParameters var distroParams []match.DistroParameters var ecosystemParams []match.EcosystemParameters var pkgParams *match.PackageParameter for i := 0; i < len(criteriaSet); i++ { switch c := criteriaSet[i].(type) { case *search.PackageNameCriteria: if pkgParams == nil { pkgParams = &match.PackageParameter{} } pkgParams.Name = c.PackageName case *search.VersionCriteria: if pkgParams == nil { pkgParams = &match.PackageParameter{} } pkgParams.Version = c.Version.Raw case *search.EcosystemCriteria: ecosystemParams = append(ecosystemParams, match.EcosystemParameters{ Language: c.Language.String(), Namespace: vuln.Namespace, // TODO: this is a holdover and will be removed in the future }) case *search.CPECriteria: cpeParams = append(cpeParams, match.CPEParameters{ Namespace: vuln.Namespace, // TODO: this is a holdover and will be removed in the future CPEs: []string{ c.CPE.Attributes.BindToFmtString(), }, }) case *search.DistroCriteria: for _, d := range c.Distros { distroParams = append(distroParams, match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.VersionString(), }, Namespace: vuln.Namespace, // TODO: this is a holdover and will be removed in the future }) } } } return cpeParams, distroParams, ecosystemParams, pkgParams } // determineMatchType determines if this is a direct or indirect match based on package names func determineMatchType(catalogedPkg pkg.Package, pkgParams *match.PackageParameter) match.Type { if pkgParams != nil && catalogedPkg.Name != pkgParams.Name { // if the cataloged package name does not match the package parameter, then this is an indirect match return match.ExactIndirectMatch } return match.ExactDirectMatch } // applyPackageParamsToSearchParams applies discovered package parameters to search parameters func applyPackageParamsToSearchParams(pkgParams *match.PackageParameter, cpeParams *[]match.CPEParameters, distroParams *[]match.DistroParameters, ecosystemParams *[]match.EcosystemParameters) { if pkgParams == nil { return } for i := range *ecosystemParams { (*ecosystemParams)[i].Package = *pkgParams } for i := range *cpeParams { (*cpeParams)[i].Package = *pkgParams } for i := range *distroParams { (*distroParams)[i].Package = *pkgParams } } // getConstraintString safely extracts constraint string from vulnerability func getConstraintString(vuln vulnerability.Vulnerability) string { if vuln.Constraint != nil { return vuln.Constraint.String() } return "" } // buildMatchDetails creates the final match details from all parameters func buildMatchDetails(matcher match.MatcherType, distroMatchType match.Type, constraintStr string, vuln vulnerability.Vulnerability, cpeParams []match.CPEParameters, distroParams []match.DistroParameters, ecosystemParams []match.EcosystemParameters) match.Details { var details match.Details // add CPE match details for _, cpeParam := range cpeParams { details = append(details, match.Detail{ Type: match.CPEMatch, Matcher: matcher, SearchedBy: cpeParam, Found: match.CPEResult{ VulnerabilityID: vuln.ID, VersionConstraint: constraintStr, }, Confidence: 0.9, // TODO: this is hard coded for now }) } // add distro match details for _, distroParam := range distroParams { details = append(details, match.Detail{ Type: distroMatchType, Matcher: matcher, SearchedBy: distroParam, Found: match.DistroResult{ VulnerabilityID: vuln.ID, VersionConstraint: constraintStr, }, Confidence: 1.0, // TODO: this is hard coded for now }) } // add ecosystem match details for _, ecosystemParam := range ecosystemParams { details = append(details, match.Detail{ Type: match.ExactDirectMatch, Matcher: matcher, SearchedBy: ecosystemParam, Found: match.EcosystemResult{ VulnerabilityID: vuln.ID, VersionConstraint: constraintStr, }, Confidence: 1.0, // TODO: this is hard coded for now }) } return details } ================================================ FILE: grype/matcher/internal/result/results.go ================================================ package result import ( "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) // Result represents a prototype match of a package used to search, a set of vulnerabilities discovered from the search, // and match details that describe the search itself. Note that all vulnerabilities in a Result share the same // vulnerability ID (in the ID field and `.Vulnerabilities[].ID` fields -- it is invalid to mix vulnerabilities into // a Result that have different IDs. type Result struct { // ID is the vulnerability ID; all vulnerabilities in this Result share the same ID. ID string // Vulnerabilities is a set of vulnerabilities that were discovered from the search. Vulnerabilities []vulnerability.Vulnerability // Details is a set of match details that describe the search itself Details []match.Detail // Package is the package that was used to search for vulnerabilities. Package *pkg.Package } type Set map[string][]Result func unionIntoResult(existing []Result) Result { var merged Result for _, r := range existing { if merged.ID == "" { merged.ID = r.ID merged.Package = r.Package } merged.Vulnerabilities = append(merged.Vulnerabilities, r.Vulnerabilities...) merged.Details = append(merged.Details, r.Details...) } merged.Details = NewMatchDetailsSet(merged.Details...).ToSlice() return merged } func (s Set) ToMatches() []match.Match { var out []match.Match for _, results := range s { merged := unionIntoResult(results) if len(merged.Vulnerabilities) == 0 { continue } if merged.Package == nil { continue // skip results without a package } for _, vv := range merged.Vulnerabilities { out = append(out, match.Match{ Vulnerability: vv, Package: *merged.Package, Details: merged.Details, }, ) } } return out } // Remove will prune elements from the current set that have any ids/aliases in common with the incoming set. // For example: // // set 1: // // Entry A: GHSA-g4mx-q9vg-27p4 (alias CVE-2023-45803) // // set 2: // // Entry B: CGA-7qjw-ggh3-pp9f (alias CVE-2023-45803) // // We want to be able to remove Entry A from set 1 because it has the same alias as Entry B in set 2. // This is because the vulnerability IDs are different, but they refer to the same underlying vulnerability. func (s Set) Remove(incoming Set) Set { // collect all incoming identifiers into one unified set incomingIdentifiers := strset.New() for id, results := range incoming { incomingIdentifiers.Add(getIdentity(id, results).List()...) } // keep only entries whose identities don't overlap with incoming out := Set{} for id, results := range s { identity := getIdentity(id, results) if strset.Intersection(identity, incomingIdentifiers).IsEmpty() { out[id] = results } } return out } func extractAliases(results []Result) *strset.Set { aliases := strset.New() for _, r := range results { for _, v := range r.Vulnerabilities { for _, a := range v.RelatedVulnerabilities { aliases.Add(a.ID) } } } return aliases } // getIdentity returns all identifiers (ID + aliases) for a vulnerability entry func getIdentity(id string, results []Result) *strset.Set { identity := strset.New() identity.Add(id) identity.Add(extractAliases(results).List()...) return identity } func unionResults(existing, incoming []Result) (n []Result) { n = append(n, existing...) n = append(n, incoming...) return n } func (s Set) Merge(incoming Set, mergeFuncs ...func(existing, incoming []Result) []Result) Set { out := Set{} if len(mergeFuncs) == 0 { // with no other merge functions specified, append all vulnerability results and details mergeFuncs = []func(existing, incoming []Result) []Result{ unionResults, } } // det all unique IDs from both sets allIDs := make(map[string]struct{}) for id := range s { allIDs[id] = struct{}{} } for id := range incoming { allIDs[id] = struct{}{} } // process each ID, applying all merge functions for id := range allIDs { existingResults := s[id] incomingResults := incoming[id] mergedResults := append([]Result(nil), existingResults...) for _, mergeFunc := range mergeFuncs { mergedResults = mergeFunc(mergedResults, incomingResults) } if len(mergedResults) > 0 { // filter out any results with empty vulnerabilities for _, result := range mergedResults { if result.ID != "" && len(result.Vulnerabilities) > 0 { out[result.ID] = append(out[result.ID], result) } } } } return out } func (s Set) Contains(id string) bool { results, ok := s[id] return ok && len(results) > 0 } func (s Set) ContainsAny(ids ...string) bool { for _, id := range ids { results, ok := s[id] if ok && len(results) > 0 { return true } } return false } // ContainsByIdentity checks if the set contains an entry with overlapping identity (ID or aliases) func (s Set) ContainsByIdentity(searchID string, searchResults []Result) bool { searchIdentity := getIdentity(searchID, searchResults) for id, results := range s { identity := getIdentity(id, results) if !strset.Intersection(identity, searchIdentity).IsEmpty() { return true } } return false } // Intersection returns entries that exist in both sets (by identity overlap) func (s Set) Intersection(other Set) Set { otherIdentifiers := strset.New() for id, results := range other { otherIdentifiers.Add(getIdentity(id, results).List()...) } out := Set{} for id, results := range s { identity := getIdentity(id, results) if !strset.Intersection(identity, otherIdentifiers).IsEmpty() { out[id] = results } } return out } // IdentitiesOverlap returns true if two results share any common identifiers, where identity // includes both the primary ID and any aliases (from RelatedVulnerabilities). This can be used // as a shouldUpdate predicate for Update when matching results by ID or alias relationships. func IdentitiesOverlap(existing Result, incoming Result) bool { existingIdentity := getIdentity(existing.ID, []Result{existing}) incomingIdentity := getIdentity(incoming.ID, []Result{incoming}) return !strset.Intersection(existingIdentity, incomingIdentity).IsEmpty() } // Update applies an update function to each result in the set where shouldUpdate returns true // for the existing-incoming result pair. The updateFunc can modify fields of the existing result // in-place while preserving other fields. Returns a new Set with updated results. // // Example with identity-based matching: // // updated := base.Update(incoming, IdentitiesOverlap, func(existing *Result, incoming Result) { // existing.Vulnerabilities[0].Fix = incoming.Vulnerabilities[0].Fix // }) func (s Set) Update(incoming Set, shouldUpdate func(existing Result, incoming Result) bool, updateFunc func(existing *Result, incoming Result)) Set { out := make(Set) // Copy everything from base set for id, results := range s { out[id] = append([]Result(nil), results...) } // For each entry in base, check all incoming entries with shouldUpdate for id, existingResults := range out { for i := range existingResults { for _, incomingResults := range incoming { for _, incomingResult := range incomingResults { if shouldUpdate(existingResults[i], incomingResult) { updateFunc(&existingResults[i], incomingResult) } } } } out[id] = existingResults } return out } func (s Set) Filter(criteria ...vulnerability.Criteria) Set { out := Set{} for id, results := range s { var filteredResults []Result for _, result := range results { vulns, err := filterVulns(result.Vulnerabilities, criteria) if err != nil { log.WithFields("vulnerability", result.ID, "error", err).Debug("failed to filter vulns") // if there was an error filtering vulnerabilities, keep them all vulns = result.Vulnerabilities } if len(vulns) == 0 { continue } filteredResults = append(filteredResults, Result{ ID: result.ID, Vulnerabilities: vulns, Details: result.Details, Package: result.Package, }) } if len(filteredResults) > 0 { out[id] = filteredResults } else if len(results) > 0 { vulnerability.LogDropped(id, "filterVulns", "no vulnerabilities matched criteria", criteria) } } return out } func filterVulns(vulnerabilities []vulnerability.Vulnerability, criteria []vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { var out []vulnerability.Vulnerability nextVulnerability: for _, v := range vulnerabilities { for _, c := range criteria { matches, dropReason, err := c.MatchesVulnerability(v) if err != nil { return nil, err } if !matches { vulnerability.LogDropped(v.ID, "filterVulns", dropReason, c) continue nextVulnerability } } out = append(out, v) } return out, nil } ================================================ FILE: grype/matcher/internal/result/results_test.go ================================================ package result import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestSet_Remove(t *testing.T) { tests := []struct { name string receiver Set incoming Set want Set }{ { name: "remove existing entries", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-1": []Result{ {ID: "vuln-1"}, }, }, want: Set{ "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "remove non-existing entry has no effect", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-2": []Result{ {ID: "vuln-2"}, }, }, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "remove from empty set", receiver: Set{}, incoming: Set{ "vuln-1": []Result{ {ID: "vuln-1"}, }, }, want: Set{}, }, { name: "remove with empty incoming set", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{}, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "remove entry with shared alias (comment example)", receiver: Set{ "GHSA-g4mx-q9vg-27p4": []Result{ { ID: "GHSA-g4mx-q9vg-27p4", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "GHSA-g4mx-q9vg-27p4"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2023-45803"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "CGA-7qjw-ggh3-pp9f": []Result{ { ID: "CGA-7qjw-ggh3-pp9f", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CGA-7qjw-ggh3-pp9f"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2023-45803"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{}, // GHSA-g4mx-q9vg-27p4 should be removed due to shared CVE-2023-45803 alias }, { name: "remove entry where receiver ID appears as alias in incoming", receiver: Set{ "CVE-2023-45803": []Result{ { ID: "CVE-2023-45803", Vulnerabilities: []vulnerability.Vulnerability{ {Reference: vulnerability.Reference{ID: "CVE-2023-45803"}}, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "GHSA-main-id": []Result{ { ID: "GHSA-main-id", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "GHSA-main-id"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2023-45803"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{}, // CVE-2023-45803 should be removed because it appears as alias in incoming }, { name: "multiple aliases with partial overlap", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln-1"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2021-1"}, {ID: "CVE-2021-2"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln-2"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2021-3"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "incoming-vuln": []Result{ { ID: "incoming-vuln", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "incoming-vuln"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2021-1"}, // overlaps with vuln-1 }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{ "vuln-2": []Result{ // vuln-1 removed, vuln-2 preserved { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln-2"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2021-3"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "no aliases in vulnerabilities", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ {Reference: vulnerability.Reference{ID: "vuln-1"}}, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{ {Reference: vulnerability.Reference{ID: "vuln-2"}}, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ {Reference: vulnerability.Reference{ID: "vuln-1"}}, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "complex transitive relationship chain", receiver: Set{ "A": []Result{ { ID: "A", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "A"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-1"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, "B": []Result{ { ID: "B", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "B"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "C": []Result{ { ID: "C", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "C"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-1"}, // matches A's alias {ID: "CVE-3"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{ "B": []Result{ // A should be removed, B should remain { ID: "B", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "B"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "empty related vulnerabilities field", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln-1"}, RelatedVulnerabilities: []vulnerability.Reference{}, // empty }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln-2"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "some-cve"}, }, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "vuln-1"}, RelatedVulnerabilities: []vulnerability.Reference{}, }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.receiver.Remove(tt.incoming) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Set.Remove() mismatch (-want +got):\n%s", diff) } }) } } func TestSet_Merge(t *testing.T) { tests := []struct { name string receiver Set incoming Set mergeFuncs []func(existing, incoming []Result) []Result want Set }{ { name: "merge with default merge function", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1-updated"}}}, Details: match.Details{{Type: match.ExactIndirectMatch}}, }, }, }, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1-updated"}}}, Details: match.Details{{Type: match.ExactIndirectMatch}}, }, }, }, }, { name: "merge new entry from incoming", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "merge with custom merge function that filters out results", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, incoming: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1-updated"}}}, Details: match.Details{{Type: match.ExactIndirectMatch}}, }, }, }, mergeFuncs: []func(existing, incoming []Result) []Result{ func(existing, incoming []Result) []Result { // custom merge function that returns empty result to filter out return []Result{} }, }, want: Set{}, }, { name: "merge empty sets", receiver: Set{}, incoming: Set{}, want: Set{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.receiver.Merge(tt.incoming, tt.mergeFuncs...) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Set.Merge() mismatch (-want +got):\n%s", diff) } }) } } func TestSet_ToMatches(t *testing.T) { testPkg := pkg.Package{ Name: "test-Package", Version: "1.0.0", Type: syftPkg.DebPkg, } tests := []struct { name string receiver Set want []match.Match }{ { name: "convert results to matches", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ {Reference: vulnerability.Reference{ID: "CVE-2021-1"}}, {Reference: vulnerability.Reference{ID: "CVE-2021-2"}}, }, Details: match.Details{{Type: match.ExactDirectMatch}}, Package: &testPkg, }, }, }, want: []match.Match{ { Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}, Package: testPkg, Details: match.Details{{Type: match.ExactDirectMatch}}, }, { Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}, Package: testPkg, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, { name: "skip results with no vulnerabilities", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{}, Details: match.Details{{Type: match.ExactDirectMatch}}, Package: &testPkg, }, }, "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{ {Reference: vulnerability.Reference{ID: "CVE-2021-2"}}, }, Details: match.Details{{Type: match.ExactDirectMatch}}, Package: &testPkg, }, }, }, want: []match.Match{ { Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}, Package: testPkg, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, { name: "empty set returns no matches", receiver: Set{}, want: []match.Match{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.receiver.ToMatches() opts := cmp.Options{ cmpopts.IgnoreUnexported(file.LocationSet{}), cmpopts.EquateEmpty(), } if diff := cmp.Diff(tt.want, got, opts...); diff != "" { t.Errorf("Set.ToMatches() mismatch (-want +got):\n%s", diff) } }) } } func TestSet_Filter(t *testing.T) { tests := []struct { name string receiver Set criteria []vulnerability.Criteria want Set wantErr require.ErrorAssertionFunc }{ { name: "filter vulnerabilities with matching criteria", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, PackageName: "test-Package", Constraint: version.MustGetConstraint("< 2.0.0", version.SemanticFormat), }, { Reference: vulnerability.Reference{ID: "CVE-2021-2"}, PackageName: "other-Package", Constraint: version.MustGetConstraint("< 1.0.0", version.SemanticFormat), }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, criteria: []vulnerability.Criteria{ search.ByPackageName("test-Package"), }, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, PackageName: "test-Package", Constraint: version.MustGetConstraint("< 2.0.0", version.SemanticFormat), }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, { name: "filter out all vulnerabilities removes result", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, PackageName: "other-Package", Constraint: version.MustGetConstraint("< 2.0.0", version.SemanticFormat), }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, criteria: []vulnerability.Criteria{ search.ByPackageName("test-Package"), }, want: Set{}, }, { name: "filter empty set", receiver: Set{}, criteria: []vulnerability.Criteria{ search.ByPackageName("test-Package"), }, want: Set{}, }, { name: "filter with no criteria returns original set", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, PackageName: "test-Package", Constraint: version.MustGetConstraint("< 2.0.0", version.SemanticFormat), }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, criteria: []vulnerability.Criteria{}, want: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, PackageName: "test-Package", Constraint: version.MustGetConstraint("< 2.0.0", version.SemanticFormat), }, }, Details: match.Details{{Type: match.ExactDirectMatch}}, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } got := tt.receiver.Filter(tt.criteria...) opts := cmp.Options{ cmpopts.IgnoreUnexported(file.LocationSet{}), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.EquateEmpty(), } if diff := cmp.Diff(tt.want, got, opts...); diff != "" { t.Errorf("Set.Filter() mismatch (-want +got):\n%s", diff) } }) } } func TestSet_Contains(t *testing.T) { tests := []struct { name string receiver Set id string want bool }{ { name: "contains existing ID", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, }, }, "vuln-2": []Result{ { ID: "vuln-2", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-2"}}}, }, }, }, id: "vuln-1", want: true, }, { name: "does not contain non-existing ID", receiver: Set{ "vuln-1": []Result{ { ID: "vuln-1", Vulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: "CVE-2021-1"}}}, }, }, }, id: "vuln-2", want: false, }, { name: "empty set does not contain any ID", receiver: Set{}, id: "vuln-1", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.receiver.Contains(tt.id) require.Equal(t, tt.want, got) }) } } func TestSet_Update(t *testing.T) { // Simple update function that replaces the Status field replaceStatus := func(existing *Result, incoming Result) { for i := range existing.Vulnerabilities { for _, incomingVuln := range incoming.Vulnerabilities { existing.Vulnerabilities[i].Status = incomingVuln.Status } } } tests := []struct { name string base Set incoming Set want Set }{ { name: "update by exact ID match", base: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, }, }, }, incoming: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "updated", }, }, }, }, }, want: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "updated", }, }, }, }, }, }, { name: "update by alias - identities overlap", base: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, }, }, }, incoming: Set{ "VULN-2023-001": []Result{ { ID: "VULN-2023-001", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "VULN-2023-001"}, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2023-1234"}, }, Status: "updated via alias", }, }, }, }, }, want: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "updated via alias", }, }, }, }, }, }, { name: "no match - base unchanged", base: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, }, }, }, incoming: Set{ "CVE-2023-9999": []Result{ { ID: "CVE-2023-9999", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-9999"}, Status: "different", }, }, }, }, }, want: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, }, }, }, }, { name: "empty incoming set - no updates", base: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, }, }, }, incoming: Set{}, want: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, }, }, }, }, { name: "empty base set returns empty", base: Set{}, incoming: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "some status", }, }, }, }, }, want: Set{}, }, { name: "preserves Details field from base", base: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "original", }, }, Details: match.Details{ {Type: match.ExactDirectMatch, SearchedBy: map[string]interface{}{"key": "value"}}, }, }, }, }, incoming: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "updated", }, }, Details: match.Details{ {Type: match.ExactIndirectMatch}, }, }, }, }, want: Set{ "CVE-2023-1234": []Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Status: "updated", }, }, Details: match.Details{ {Type: match.ExactDirectMatch, SearchedBy: map[string]interface{}{"key": "value"}}, }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.base.Update(tt.incoming, IdentitiesOverlap, replaceStatus) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Set.Update() mismatch (-want +got):\n%s", diff) } }) } } ================================================ FILE: grype/matcher/internal/utils_test.go ================================================ package internal import ( "testing" "github.com/go-test/deep" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/vulnerability" ) func assertMatchesUsingIDsForVulnerabilities(t testing.TB, expected, actual []match.Match) { t.Helper() require.Len(t, actual, len(expected)) for idx, a := range actual { // only compare the vulnerability ID, nothing else a.Vulnerability = vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: a.Vulnerability.ID}} for _, d := range deep.Equal(expected[idx], a) { t.Errorf("diff idx=%d: %+v", idx, d) } } } ================================================ FILE: grype/matcher/java/matcher.go ================================================ package java import ( "context" "fmt" "net/http" "strings" "time" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" syftPkg "github.com/anchore/syft/syft/pkg" ) const ( sha1Query = `1:"%s"` ) type Matcher struct { MavenSearcher cfg MatcherConfig } type ExternalSearchConfig struct { SearchMavenUpstream bool MavenBaseURL string MavenRateLimit time.Duration } type MatcherConfig struct { ExternalSearchConfig UseCPEs bool } func NewJavaMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, MavenSearcher: newMavenSearch(http.DefaultClient, cfg.MavenBaseURL, cfg.MavenRateLimit), } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.JavaPkg, syftPkg.JenkinsPluginPkg} } func (m *Matcher) Type() match.MatcherType { return match.JavaMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match if m.cfg.SearchMavenUpstream { upstreamMatches, err := m.matchUpstreamMavenPackages(store, p) if err != nil { if strings.Contains(err.Error(), "no artifact found") { log.Debugf("no upstream maven artifact found for %s", p.Name) } else { return nil, nil, match.NewFatalError(match.JavaMatcher, fmt.Errorf("resolving details for package %q with maven: %w", p.Name, err)) } } else { matches = append(matches, upstreamMatches...) } } criteriaMatches, ignores, err := internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) if err != nil { return nil, nil, fmt.Errorf("failed to match by exact package: %w", err) } matches = append(matches, criteriaMatches...) return matches, ignores, nil } func (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) { var matches []match.Match ctx := context.Background() // Check if we need to search Maven by SHA searchMaven, digests := m.shouldSearchMavenBySha(p) if searchMaven { // If the artifact and group ID exist are missing, attempt Maven lookup using SHA-1 for _, digest := range digests { log.Debugf("searching maven, POM data missing for %s", p.Name) indirectPackage, err := m.GetMavenPackageBySha(ctx, digest) if err != nil { return nil, err } indirectMatches, _, err := internal.MatchPackageByLanguage(store, *indirectPackage, m.Type()) if err != nil { return nil, err } matches = append(matches, indirectMatches...) } } else { log.Debugf("skipping maven search, POM data present for %s", p.Name) indirectMatches, _, err := internal.MatchPackageByLanguage(store, p, m.Type()) if err != nil { return nil, err } matches = append(matches, indirectMatches...) } match.ConvertToIndirectMatches(matches, p) return matches, nil } func (m *Matcher) shouldSearchMavenBySha(p pkg.Package) (bool, []string) { digests := []string{} if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok { // if either the PomArtifactID or PomGroupID is missing, we need to search Maven if metadata.PomArtifactID == "" || metadata.PomGroupID == "" { for _, digest := range metadata.ArchiveDigests { if digest.Algorithm == "sha1" && digest.Value != "" { digests = append(digests, digest.Value) } } // if we need to search Maven but no valid SHA-1 digests exist, skip search if len(digests) == 0 { return false, digests } } } return len(digests) > 0, digests } ================================================ FILE: grype/matcher/java/matcher_integration_test.go ================================================ //go:build api_limits package java import ( "context" "net/http" "strings" "testing" "time" ) // TestMavenSearch_GetMavenPackageBySha tests the GetMavenPackageBySha method of the MavenSearch struct. // This is an integration test and requires network access to search.maven.org. // It is not intended to be run as part of the normal test suite. // Use this to validate rate limiting in [maven_search.go] and the ability to fetch package data from maven.org. func TestMavenSearch_GetMavenPackageBySha(t *testing.T) { ctx := context.Background() ms := newMavenSearch(http.DefaultClient, "https://search.maven.org/solrsearch/select") // Known SHA1s to test with, using a large number of known good SHA1s to validate rate limiting // This is not typical but for Images with a large number of Java packages, this is a good test // to ensure that the rate limiting is working as expected and we don't silently fail and loose scan results shas := []string{ "bb7b7ec0379982b97c62cd17465cb6d9155f68e8", "b45b49c1ec5c5fc48580412d0ca635e1833110ea", "245ceca7bdf3190fbb977045c852d5f3c8efece1", "485de3a253e23f645037828c07f1d7f1af40763a", "97662c999c6b2fbf2ee50e814a34639c1c1d22de", "21608dd8b3853da69c4862fbaf9b35b326dc0ddc", "a9cd24fe92272ad1f084d98cd7edeffcd9de720f", "eab9a4baae8de96a24c04219236363d0ca73e8a9", "3647d00620a91360990c9680f29fbcc22d69c2ee", "b957089deb654647da320ad7507b0a4b5ce23813", "bd0cd7ad1e3791a8a0929df0dcdbffc02fd0bab4", "0d1efd839d539481952a9757834054239774f057", "f6148c941e4ec2f314b285e6e4e995f61374aa2f", "502008366a98296ce95c62397b1cb7e06521a195", "92b2a5b7fb0c6a8dcd839d98af2e186f1e98b8ca", "64e6d9608f30eefbe807e65c148018065f971ca6", "095454c18fb12f8fcdbeae4747adfa29bfe6bf17", "0322a158f88b2a18b429133d91459dfa38bf9f55", "f18ebbe9a3145b9ce99733f5a0b7d505be9ae71e", "526df0db4c22be3eb490dab2b4ef979032e3588d", "521694be357010738e7bc612089df8fcc970a0d5", "50d87efaed036c7df71f766ca13aa8783a774ce9", "e8b2cbfe10d9cdcdc29961943b1c6c40f42e2f32", "3c0daebd5f0e1ce72cc50c818321ac957aeb5d70", "919f0dfe192fb4e063e7dacadee7f8bb9a2672a9", "8ceead41f4e71821919dbdb7a9847608f1a938cb", "a1678ba907bf92691d879fef34e1a187038f9259", "83cd2cd674a217ade95a4bb83a8a14f351f48bd0", "6b0acabea7bb3da058200a77178057e47e25cb69", "31c746001016c6226bd7356c9f87a6a084ce3715", "cd9cd41361c155f3af0f653009dcecb08d8b4afd", "2609e36f18f7e8d593cc1cddfb2ac776dc96b8e0", "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8", "ca773f9985c9f4104d76028629026c69c641923c", "a231e0d844d2721b0fa1b238006d15c6ded6842a", "8e6300ef51c1d801a7ed62d07cd221aca3a90640", "379e0250f7a4a42c66c5e94e14d4c4491b3c2ed3", "4b071f211b37c38e0e9f5998550197c8593f6ad8", "1f2a432d1212f5c352ae607d7b61dcae20c20af5", "a3662cf1c1d592893ffe08727f78db35392fa302", "78d2ecd61318b5a58cd04fb237636c0e86b77d97", "5b0b0f8cdb6c90582302ffcf5c20447206122f48", "0d8b504da88975fdc149ed60d551d637d0992aa1", "507505543772f54342d6ee855fa8f459d4bc6a11", "71abe1781fa182d92e97bf60450026cc72984ac2", "e0efa60318229590103e31c69ebdaae56d903644", "8ad1147dcd02196e3924013679c6bf4c25d8c351", "9679de8286eb0a151db6538ba297a8951c4a1224", "73b9a0e7032a5ae89f294091bc6cbb9a67a21101", "152f846d9f30a3e026530c2087ecd65c39bb304b", "73de3b1233c1da8fd46f9a4bd8ebec97890af9dc", "25d54640c4a17aa342490c4c63c172759361bf56", "eca76e00f897461f95bbb085f67936417ae03825", "802b5b3de0a38e71f07aa3048f532cd1246bc5af", "10d40ab670bf1fa53c925462f84f43507cf3b9bc", "2a14a2ff74f6ec3546b257889949630d3b2a0dbb", "357efe3f93c58bc4a10d40b1301045405b8a9f73", "570430f532b1e98c5d72a759ccbe7851099cee5f", "3174a146b81819fe2cd42e23081cd902ac743a8d", "940873068ea1383f4d962613cc1eca7c8cecc00e", "2116ab332c0bedfd038ad9d39c2e17219abf34aa", "527f9c5ccc6b76ad6e88ca571272a6a2ea535921", "04d21d5e6b71b2634dc67b36bf9b2defce7a7cc3", "37a5a4660941852c298e4caf4592b46b98ce512c", "780be6395b7c65d8d90ca2e1c3c2a46c46c5a154", "6251d68d3039f7b215b205f0e61cb2d732e5bc9b", "1d7efb089db2fe7a60526b8ff50b0c681fe1b079", "1f21cea72f54a6af3b0bb6831eb3874bd4afd213", "cd58e9e1b3ece090edd60a072f66b6cf52bce06d", "fcfd07e6ad0b5eadb0af1bddcc7b04097dacad7c", "e6fdf0f32f49d2a2380f5b458469052c272f8d9b", "324669468c32535f19bc4791fcaa34f2ed82200a", "ba584703bd47e9e789343ee3332f0f5a64f7f187", "17b3541f736df97465f87d9f5b5dfa4991b37bb3", "39e9e45359e20998eb79c1828751f94a818d25f8", "5353ca39fe2f148dab9ca1d637a43d0750456254", "603d37b2a108e2b437bb9b3b2ffb5962b4aa198c", "6000774d7f8412ced005a704188ced78beeed2bb", "537a3281dfefbd7939d27785732a2aafddd3abcb", "92446d8dfc8e57289e6120a7efc6932650ed3410", "eacefc2460e0ac5fe2ad48a9b0ffced5aea451b9", "4314021484adf9b32b3ae5421fac6fe0ed56e53e", "5786699a0cb71f9dc32e6cca1d665eef07a0882f", "2bd4f1921c78c2adffbe2eb01117c7936d0a0789", "de2b60b62da487644fc11f734e73c8b0b431238f", "e752540aeccb620f23c1e2f15c4c707254f6f596", "638ec33f363a94d41a4f03c3e7d3dcfba64e402d", "3fe0bed568c62df5e89f4f174c101eab25345b6c", "17773f342aabf0b177c9e3b8d8396d851cbfe64e", "1ae01f9be1cabf50ee735383a9fc3342e778c17e", "bf76d02e2be0dd8f99f106658ea7cacfa8df69d1", "f82b463a5c9eadb2a6667a1cb51b46d8d8d8d69b", "073e532b7cf87928bcd2512a0faf1151f8bd199a", "0912e12e4c7dc1c87ea8574065725a63342cf19d", "d52b9abcd97f38c81342bb7e7ae1eee9b73cba51", "dc98be5d5390230684a092589d70ea76a147925c", "47bd4d333fba53406f6c6c51884ddbca435c8862", "8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5", "54ebea0a5b653d3c680131e73fe807bb8f78c4ed", "19d5bfd402f91de0e670ef5783bf5c0a3f5ab478", "659feffdd12280201c8aacb8f7be94f9a883c824", "2b681b3bcddeaa5bf5c2a2939cd77e2f9ad6efda", "30be73c965cc990b153a100aaaaafcf239f82d39", "dc887691eab129c5728e26b095751fcadd36719d", "ddcc8433eb019fb48fe25207c0278143f3e1d7e2", "0ce1edb914c94ebc388f086c6827e8bdeec71ac2", "c6842c86792ff03b9f1d1fe2aab8dc23aa6c6f0e", "5043bfebc3db072ed80fbd362e7caf00e885d8ae", "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f", "e4ba98f1d4b3c80ec46392f25e094a6a2e58fcbf", "4572d589699f09d866a226a14b7f4323c6d8f040", "bd1a6e384f3cf0f9b9a60e1e6c1c1ecbbee7e0b7", "3363381aef8cef2dbc1023b3e3a9433b08b64e01", "3833ca68f9f42fd11d4e0a036e9a3faae5d5f1a8", "4316d710b6619ffe210c98deb2b0893587dad454", "c22383d089321fd0c58a15c1c6ef5d24b5b5ee0c", "d858f142ea189c62771c505a6548d8606ac098fe", "66d618739859bc75ab9643b96a9839ac7802ec90", "e3aa0be212d7a42839a8f3f506f5b990bcce0222", "d25497d443d0843dbf2973e802c06722f2cb4578", "db2d83bdc0bac7b4f25fc113d8ce3eedc0a4e89c", "b706a216e49352103bd2527e83b1ec2410924494", "4aa0cfb129c36cd91528fc1b8775705280e60285", "4e1cce64b1ec11080a01172a0c296431d9469294", "e3fdd7fa9255bba0a206aea059cf133565c48cbd", "f0f717ed3495ed2e58d96e0084f73db0c7b3ba3d", "a79cf96a15f4b5376fae0024c0b0cd44cfa8a295", "49c3df840c2268479fb8f5cfd7df023bd6927bc9", "9d9d56fcae37f1b3d48d80f8b7eefabd3477569d", "09bfca4ee4f691f3737b3f4f006d0c4770f178eb", "0a1ed0a251d22bf528cebfafb94c55e6f3f339cf", "bc5b0c72a3755de7f3dca9f059aa19cc9d27a843", "a096cfeb58b927dde6b80ad295e564513514f9be", "b1e952300954b6d33911ba29a984455fcc3f1024", "5774f912db3dca1e9049af15cce6a4f7845a173d", "a2f8cf63192ebba929451a221cc382bc0ca5abb7", "13e3663d5878001666981eb5ef6efb22fa6799bb", "9697b9e1667b4f2daa9ea454b4a0e0f905585c8b", "32088dfde15a3f8ad4f2547cb083777afddc12d5", "fabcda911ebc80e3a9b6064863da4f2e5094814f", "2c3591cf5e2f5de644aae09a73a896f0c7964f43", "bce88f90c3341ed14df2ce3919f253334cd834f2", "df7bbc5a4c8304aa8aed34cb67e339035ac2c34b", "c305f6229dde8f3946de5574ac9779309073f2e3", "ad63993db3525be5e290e0ccb3d5122c01bd356d", "24b20d4f91c894e19947389d3040adfc174a6af1", "58c3d2641b48a9db2e29009f42077dcd70f7e351", "8450fb3261e7ec1d734c2b11ca4d875fe82386eb", "9a296a2da46d296f3d0b78d3941ec468c64ba3e6", "bd7b0f03050125e8dd8bd9498e34561e1e88db03", "b6104ad646d672770561918073f1aaacb7c7b341", "8c177eb55da21bee1cd654d66241b98fb0e44c86", "1c45fbaa5f4d66070b7f1ee5e4653aadb14aa97d", "0ed231cd84006f5fdfda7671beae2b9b41a2dafa", "8ed4fee000f82e6248f7f8cfdd11d53fe03f98ad", "302ebf7b124c9a037333a9b81a5f2ce0880f8a29", "eba91bffe866a695d145c5e1692509f92de5b23b", "5a1f4a878b75dbcfbf0d4ae783bf1c1229309470", "5351a31139b9b5e3f8d50252ac081249b1ad00fb", "fdc6f7632078dc5b570f9120d9ab07892e784554", "4a0126da8cf7794e913a13e3f8f4ab62ca5e2981", "4a3df17312a2ab95a4d75396065079aebfb2a1e7", "51fac22c802ae94247664efd95a1e60d138a278d", "c6dd14eb5a4abfcf1c8dbc7187c2ec3b8d9be1f9", "96d70f8f82a534438b938f86b3a6682eb34824ca", "e714165da098686f600d75b914448fdd4a057d60", "60ba0670d68758e893870079916954a7f01afe23", "320e7d1fdbab2bffb8138d66c24724cc24ea654c", "404840df034905ae2b5a9c922639e1d9f694516d", "63f0c49628c9695704d0014409f030d82bc10f70", "68186dc73e3d123999ebb93a6d5a5d0bbb4d4e91", "9ba2ed9f74f5122f25113cd6d5e14fbc442c867f", "993a5608c4942b5b81a6c14fd78779d024e6ed41", "f07ec0309a1e37629f097408e1cb75f7d0ea58c5", "1ffcac9e1bbd3d00db1e2089d8e915f20c0ac568", "9450776e99a5a1b413b98cb095f6fe7f81935c3d", "c72b35c5dea306de35ad0ff207eff4d14b37b880", "5e4e7abcdb8f4101b9aa0ba84658be21c445b1d5", "ef74ce50e19736bf72341a572c1ad6fd2ba6c3fe", "55a266187baa9d1c68447ff6ab404a4324de7935", "8a65a223354726586d95f45aa8f6175ca23b784c", "2ddd12523600e8b80d2be0bc003cd447bd2751d4", "19160c71c598866e9c96af667045c886c8dc9b48", "163372f10bf5f028ccbb122eebc9cd2deb30b094", "566ab030e0a0f010dfe0d185b0804b53817db7ec", "0c74d5b6c2ef578266361a58ec7c848cd844f2bd", "452cd7f4850757ad76710cea53bd9ad8d181d5dc", "98cbe204421b538fd2fbf4a1ce689f8398bd2ced", "b917f21f99eeacf49f55e8fd089b93119c7dbd9b", "bd4d8f4a02886a26b60c76048547a453691fcec3", "1dcf1de382a0bf95a3d8b0849546c88bac1292c9", "799748e42a644db85394db066af658809f89c523", "c693557ee87e311340eb0f8a811b8bca027af421", "912b86862ad070dd3d21f51e05e361eba1f515da", "67b085271fd9cc0a61eb04fcaf288ad35b2e7995", "a41a8b5641dad26c7601ea93818611b4a6465058", "5ede807d3bcdace2e25d5614382bfdf1663012e5", "8275c3b8829eb16a54fb49ceda2f6fbb44546c26", "5a2b47396587b499575782b60cb223a830bc86d7", "e113ac14fb2b70c1510f92ea2a0405ba4da01f5c", "10e53fd4d987e37190432e896bdaa62e8ea2c628", "286c93b65ab3c3a0a257b0a6ebdd99c06c674c88", "7b93e7e3c64b837b69da7497fdf4c28b677625bf", "0bc23b2c7e6419d3cd7e108d6942b9431bf5c25c", "0a5f0e4a16f5b12cde3df1ea413852aeaf176176", "44984c2480ac8aaef4a660a06565aa76c577238c", "5878d0f20e7cc521a437217dd21c3a84788d3a53", "73120785e720701d1142d97bdc72bf5d6b5af4bd", "fee8f41ab7f59597e35d8a6eb01b9edc9b04d51e", "e0feb1bd93ad9fb1e064706cff96e32b41a57b9c", "3dc8cea436c52d0d248abe9648b0e4f1d02bd500", "3e224b1b9e18dd28c89a764b1feea498ba952579", "09d6cbdde6ea3469a67601a811b4e83de3e68a79", "3251b36ee9e9c3effe3293f8d7094aa3841cad55", "252e267acf720ef6333488740a696a1d5e204639", "789cafde696403b429026bf19071caf46d8c8934", "4a4f88c5e13143f882268c98239fb85c3b2c6cb2", "8046b9d6b423f24457cfb20210d0ee8abc98e22c", "bb4eda2c61102759e7c03ab12ff7c19547e20cbd", "52da76c0c8190be88281aec828efd44df176ab34", "878e2200222f5e11137d5bfde325a5db30687592", "1789190601b7a5361e4fa52b6bc95ec2cd71e854", "44f8a2b2c0dfb15b4f112e22de76d837b89bd4d6", "aaf681a518ce5c9a048328b86ba5b9c5123375aa", "dbd77d2e6c54ed9fafc83f1cf6f48342250996d7", "532fd1449686690273222ebd5cb86c233ed19f58", "60de19e6c8e44b1a78acb0dd73722b2feaa7ccfe", "85289261815e7d2fb1472981652fce50ae8cfc42", "71b610fca525744bc70eb96c9f9113cddbc38f4f", "2977cca2c82e3c5336805ebb6226c14137585b54", "1d2d1a5ed9cfc58d0a7bdc1d9dda5ecb9987da9a", "d339db49e637d2a8122a67ad846a294124f1a2f3", "02f16015ed9e2689e10f86f1b7c3522e541c0c75", "0e7eab15d6b4184921b82fbca6f89dcb60ea972e", "362e2295d95b2e2797457760eef1f172d07d7417", "c067143934cb76530adbb8fd4e2df1ab737a16e0", "b3add478d4382b78ea20b1671390a858002feb6c", "907df2bf39d70510951b7bafbf661f286eed90a5", "6e5d51a72d142f2d40a57dfb897188b36a95b489", "00f6db9a5e6fe2374b5a494b08388c2b6e0792d8", "eeb69005da379a10071aa4948c48d89250febb07", "af799dd7e23e6fe8c988da12314582072b07edcb", "3b27257997ac51b0f8d19676f1ea170427e86d51", "90ac2db772d9b85e2b05417b74f7464bcc061dcb", "451bc97f7519017cfa96c8f11d79e1e8027968b2", "066aaf67a580910de62f92f21f76e3df170483cf", "d5e162564701848b0921b80aedef9e64435333cc", "12ac6f103a0ff29fce17a078c7c64d25320b6165", "21f7a9a2da446f1e5b3e5af16ebf956d3ee43ee0", "81065531e63fccbe85fb04a3274709593fb00d3c", "09ca864bec94779e74b99e84ea02dba85a641233", "d635e3eed4beb74213489ff003ca39dbe47ea44e", "b75e5e9feb70f599a6f6232e71bd5b0030608179", "02419d851c01139edf9e19b81056382163d9bfab", "57b7ba0ca94313c342b03bd31830fe4a8f34bc1a", "a68959c06e5f8ff45faff469aa16f232c04af620", "70b332574395cde2c56db431b619be9823407aed", "45c3bb7696f29655189abb78ec1c97f511643159", "99a1348743f3550dd4524408725efab8eb319960", "acc766c65cd4e94a5e1fab6a2f85148dfc8613d8", "28bdcdcceb92a1ac450b8b6a3d3d0627d839054d", "9ea12cb2c426d521b7c4cad5b02ce18e5b614d4e", "8347ef8861b75bbffcaebb706a0ae296daabc20e", "e844c4278ecba985c08e0dea1181343a07c04c3e", "4b151bcdfe290542f27a442ed09be99f815f88e8", "1d7200e19d1ffdaf6927ff0be701724c85be07d7", "1861e32c3c484a1aa5f55ab109e08dd1b32c6fa2", "34c56f43fd3255fc239ffe33d0fbfb8195be6a24", "e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada", "0c900514d3446d9ce5d9dbd90c21192048125440", "56b53c8f4bcdaada801d311cf2ff8a24d6d96883", "de748cf874e4e193b42eceea9fe5574fabb9d4df", "4bafcb5aacb1abc193698a13ae99394e09a25101", "687cede1a44f70c7741abfab6ee2aa53dd2bfb54", "34d8332b975f9e9a8298efe4c883ec43d45b7059", "698bd8c759ccc7fd7398f3179ff45d0e5a7ccc16", "2872764df7b4857549e2880dd32a6f9009166289", "164343da11db817e81e24e0d9869527e069850c9", "e1c6222f2fa8d05d7825d8e9af7b9bef089c0b5e", "127fc12785c42eeff7da15abca690655add7c710", "53c041061964825372701d75b96a67e82bc3b6da", "ba4bfac0366399080e878cb5c41023c3eb7f7328", "61ad4ef7f9131fcf6d25c34b817f90d6da06c9e9", "4a3ee4146a90c619b20977d65951825f5675b560", "67613a3a83092588b85b163b9aeb3e87ec46b4ea", "c197c86ceec7318b1284bffb49b54226ca774003", "ba035118bc8bac37d7eff77700720999acd9986d", "c85270e307e7b822f1086b93689124b89768e273", "2042461b754cd65ab2dd74a9f19f442b54625f19", "04669a54b799c105572aa8de2a1ae0fe64a17745", "48d6674adb5a077f2c04b42795e2e7624997b8b9", "15177b3e1c91529ba02c056035d71463f9e66a03", "241aa26c61f638b7e76787558bb1be49984f2a0d", "4605d2f4267388d02d810a3cea448a48371435a3", "d1cce505a1dafd5b5d842ec8f91105ccca7d5e4d", "0a10ae77a57942352f3d2abe5d58b199b1f83d33", "dc6c49c40b1d5acf3ee58784ec34360806f48a22", "6d9393fd0ea1e1900451b5ee05351523725392bb", "8a603c50591cd2ba1039afa3f28540b0f43c82c5", "b4bd511b8cff2cfd83faf37f48b6e256dabcfc6c", "c02f6844c6d27c8357257eab162250b54d38390b", "46cb8029e744ac128d4ce58c944ab21ab7ef3e25", "e27eb84680badf95422bf5798faf8b0b7fa332f4", "a974a4972c0ecd2206b045e387bea4713a5451b6", "e3480072bc95c202476ffa1de99ff7ee9149f29c", "74548703f9851017ce2f556066659438019e7eb5", "562a587face36ec7eff2db7f2fc95425c6602bc1", "f3cd84cc45f583a0fdc42a8156d6c5b98d625c1a", "f48473482c0e3e714f87186d9305bcae30b7f5cb", "288f60226c596849c3c57926e8421c83b5abbe87", "5eacc6522521f7eacb081f95cee1e231648461e7", "5eea182d6651a7257bc8c3614507e1540c766fc2", "8d49996a4338670764d7ca4b85a1c4ccf7fe665d", "48e3b9cfc10752fba3521d6511f4165bea951801", "0422d3543c01df2f1d8bd1f3064adb54fb9e93f3", "878a02f3ab98d37206c9852c025a46b86dc882d0", "c8de82b962142a5f3d408ecc3920642b166de028", "bf744c1e2776ed1de3c55c8dac1057ec331ef744", "85262acf3ca9816f9537ca47d5adeabaead7cb16", "934c04d3cfef185a8008e7bf34331b79730a9d43", "60a59edc89f93d57541da31ee1c83428ab1cdcb3", "935151eb71beff17a2ffac15dd80184a99a0514f", "3cd63d075497751784b2fa84be59432f4905bf7c", "8531ad5ac454cc2deb9d4d32c40c4d7451939b5d", "3758e8c1664979749e647a9ca8c7ea1cd83c9b1e", "dd6dda9da676a54c5b36ca2806ff95ee017d8738", "40fd4d696c55793e996d1ff3c475833f836c2498", "ef31541dd28ae2cefdd17c7ebf352d93e9058c63", "d877e195a05aca4a2f1ad2ff14bfec1393af4b5e", "39e8f0d32258f304928b29bc7e1f7d85fa5ae218", "f9414e9ed1a16132c5d3467991a3bebc4367a1bc", "984a623e0c0dfec82cb1cd390ee1aab51fa02bee", "d60d3f8ccefd848d551d25bcb7f3e9251333648b", "e9cb36b5954d1af1593bbc4fecadfa5cb170bd44", "058e7a538e020b73871e232eeb064835fd98a492", "6f14738ec2e9dd0011e343717fa624a10f8aab64", "7aaae28e06aafe63ac94f7c6dee81135b815db92", "9b1f3cf3fdd02d313018f1a67c42106e6ce9f60d", "21c5319c82ca29705715b315553a16f11b16655e", "fc5cdccdeeedb067b8b2a3c7df2907dfb7e8a1b9", "44df9a1310c1278b62658509aca3ca53978e8822", "fbdcb39db6a6976944a621fe11bf1d2ff048d7c2", "1a01a2a1218fcf9faa2cc2a6ced025bdea687262", "056dcc8480ecd2c03ec004aa76278d1f2d621561", "b33d6d9045da8f0b317162facabcd1dc9ebf751d", "be8f9b519d692dfd1e2726ee9e26573dabc99e70", "e522a857d234e5ade679abfae807bcf4ecdd6f2e", "533e7cbc5efc1d58d14cefb68904cd3af47fc316", "fdeb0e5beb9ddbfb49b4aec3daa55d71e0cf1956", "966952ede72900ddaa20888beddc86a5a002cbd3", "b22ab0397e893d9c092ade34a8e826ef576b285c", "65ce500f7cad946ce0e172c6ce90319caa29e787", "c8645a939af24f227e4123b89af14264176f7c60", "62bbe12f1a737d9b96358a9964466ffea6a6487a", "696cf9f9160e3a0ff208db614af118915cbdcf2d", "2f2695de1ae62d84ca8336c7e6ddedc80aa2e521", "59d5e3e86e2583fba0cf04d4f126a5205a24c4b6", "d60bb33b97b968b555ce829961d41971ff826415", "1200e7ebeedbe0d10062093f32925a912020e747", "88e9a306715e9379f3122415ef4ae759a352640d", "0a1cb8dbe71b5a6a0288043c3ba3ca64545be165", "a240efb690601cb1ef02a6778a23e450559b0bef", "0e7c45d7667feb56e5664247a882451c3d438def", "eaa7ec646db93f0096ca8100c361018c2608319b", "cef76bca08c1f437150890d1b8bf430a66ebe42a", "21743fe8af7bb684d5ebbd4075f397fccc30d158", "006936bbd6c5b235665d87bd450f5e13b52d4b48", "698ce67b5e58becfb4ef2cf0393422775e59dff4", "a698fd936b13588c6747b182bcfdc13885a8ca43", "357a3836bb5da16f314f3a1e954518e5468cd915", "37fe2217f577b0b68b18e62c4d17a8858ecf9b69", "5e303a03d04e6788dddfa3655272580ae0fc13bb", "c9ad4a0850ab676c5c64461a05ca524cdfff59f1", "cc5888f14a5768f254b97bafe8b9fd29b31e872e", "63f943103f250ef1f3a4d5e94d145a0f961f5316", "516c03b21d50a644d538de0f0369c620989cd8f0", "25ea2e8b0c338a877313bd4672d3fe056ea78f0d", "59033da2a1afd56af1ac576750a8d0b1830d59e6", "2ca09f0b36ca7d71b762e14ea2ff09d5eac57558", "3ff3baa0074445384f9e0068df81fbd0a168395a", "4c7018119aeb66335746e6748456c821e304d3a2", "75a75c47eb912f3fd06df62a9e4b3b554d5b2bec", "2e617bd795b3b55b2ec23543721665a2b1c77b9c", "a0f0c4de6cc321130252e86658c21b2e1b6af008", "7868b29620b92aa1040fe20d21ba09f2506207aa", "a82d2503e718d17628fc9b4db411b001573f61b7", "e358016010b6355630e398db20d83925462fa4cd", "82357e97a5c1b505beb0f6c227d9f39b2d7fdde0", "66eab4bbf91fa01ed4f72ce771db28c59d35a843", "eb91bc9b9ff26bfcca077cf1a888fb09e8ce72be", "c56ffb4a6541864daf9868895b79c0c33427fd8c", "1e39adf7c3f5e87695789994b694d24c1dda5752", "93d37f677addd2450b199e8da8fcac243ceb8a88", "d54a9712c29c4e6d9d9ba483fad3d450be135fff", "a4c3885fa656a92508315aca9b4632197a454b18", "4c1fd1f78ba7c16cf6fcd663ddad7eed34b4d911", "389b730dc4e454f70d72ec19ddac2528047f157e", "7d1b5b69a5ea87fb2f62498710d9d788d17beb2b", "b8af3fe6f1ca88526914929add63cf5e7c5049af", "dafaf2c27f27c09220cee312df10917d9a5d97ce", "7473b8cd3c0ef9932345baf569bc398e8a717046", "67f57e154437cd9e6e9cf368394b95814836ff88", "3c2af9d14e43d46b541ac1a0cdd7be4980aeed84", "aec7142dafa1f96154018eced507854bb544cf41", "319d3da49ca42fca687de2accef1d22fba786405", "7577f792244cae44227e675aafcf6597a2eeb00d", "9df166a4a89314f5281b37e524bb366d7ffadf23", "a0b8af84c1ddf5d9dd7d1eef8a19df527864e8cd", "ba91c4fa57b566baff698a3354db2b8af8626d3c", "7d5c94b0fb6384b91d963d6d398468d96bb4983f", "4c54840ac217908029e77a96336c03901a6776d5", "6ac92abcc06bb8d52d8179889c6b1173ba7bd027", "8bc95edbea781cc09dc6755f570b72a993df1679", "a9992b9918cd8582e2fef748a61ec1c46894b13f", "bb8b60297070d7c352a06c6bbc3854cfac26d46a", "4185a5807b9fdbdcbee813f8e82ceb433ad75c68", "9bc1a58a9472452100f873059683a9a37e37063c", "4d1701b9c993f5edfd232ea06a6f8ba540113b59", "5d5c7c1b342c89b4b0d28cd99572827cbe3e6f15", "d641ffaf2a3a84c8c85d24850b916c5baf547a38", "c5bf5e88b285eda01f3d2044c76a6f5651dfa4c0", "6b7aca9462a226dbdef319e6875523e322c6f80b", "b9740e040f5fc06920f38d9fafd966bfd3cabe1e", "830a7d5e13edb5f6f81c36877edf68b55a4182fb", "751030d0b6c06337bb2870cd174ec83ed8417b3e", "b8957915e5d02d9e341eaa07a90019aeb90d546f", "82cfcd48e0c239ddbdf4fa122b8715275b761de2", "97c73ecd70bc7e8eefb26c5eea84f251a63f1031", "5d1abb695642e88558f4e7e0d32aa1925a1fd0b7", "0e5af3b6dc164eb2c699b70bf67a0babef507faf", "f08a912ce02debbaa803353686964b3c5fcfdb53", "8625e8f9b6f49b881fa5fd143172c2833df1ce47", "b421526c5f297295adef1c886e5246c39d4ac629", "9be9bb9b3a1638dcd948edd6179bd8ee4ffcc137", "bea6fede6328fabafd7e68363161a7ea6605abd1", "7183a25510a02ad00cc6a95d3b3d2a7d3c5a8dc4", "af40e34de7c30e4fef253024a88257f6dddc547a", "d2cac68225a25a5486ea848af95680573ee3d393", "d952189f6abb148ff72aab246aa8c28cf99b469f", "87fa769912b1f738f3c2dd87e3bca4d1d7f0e666", "79d7792942fa009316de2d7d1a4d7e8b33548947", "6604030f7da573a8c00641f9c7deef6c143b6022", "2746f9ec96f9ce3a345b11f03751136073f7869f", "f73773fc39d43df7661609b9f7a733ddfd091af7", "be8a20787124cf52c56c5928ef970df2d8a26f51", "8ec1dce97ba5b616e165068225bba873179482e9", "dff1e225fe6bfdf7853663bc48831e9714bf035e", "ebe2549568386d5c289ec0eb738172f1a0445259", "9d5fdc88f91586bf5d1afa13b9a77302c39b5e7c", "ab7c7c3c823cb2f8fb1b54fdc82b3e133e8e8344", "bc34429b8d1a620c58639f376bee9ba425a035d3", "0bc8d9f00bd34806bc82d01390855ef9dcbea85b", "a1e978879d35af3590549437b80679b5c00f27d6", "1000c919125bb13f265b101341c34bb5af814fd3", "488e5cfdd4d2d30b161fc45819a82a6984eb0f99", "4b986a99445e49ea5fbf5d149c4b63f6ed6c6780", "64485a221d9095fc7ab9b50cc34c6b4b58467e2e", "bf9e9aea47c3d112929fc56abd75a48d31914fda", "73334ff5470db03e5b793ce1d5854642b2c21799", "7fd8a65d950d0e77dd39cc4ce2776ff9673ae470", "d4c0da647de59c9ccc304a112fe1f1474d49e8eb", "ccf79a1a63ef35de038a4226a952175c4e9f4f59", "5fb53c92da84ebeff403414b667611d6bcd477cf", "ef5bccf2a7a22a326c8fe94e1d56f6f15419bedd", "311d38cf15ec7f5c713985862632db91b7a827af", "e2d5e96ea4bbd4fc463dbb76d07dd8aefac05e3c", "61625cf2338fe84464c5d586dbba51d4ff36a2b8", "9d8cd3ed749f2c2f846e0c58c485c8a0d5d5181e", "2f23beded3e46a3552fc3c1a0fdfb810c24d8f97", "54bc99d2b886a868d79d537ee5e7829bb062fbe4", "52fd60d5dc3f0fb3ed5c19b63f6f2312cd1f6add", "8a8ef1517d27a5b4de1512ef94679bdb59f210b6", "f6ea1e9c0a0acf137a8a4c5353bc97ead6b82cf7", "ea93fbd2137c797ed8a686737e8bdfeead20f1b1", "18ed04a0e502896552854926e908509db2987a00", "2a9d06026ed251705e6ab52fa6ebe5f4f15aab7a", "c2ef6018eecde345fcddb96e31f651df16dca4c2", "93cc78652ed836ef950604139bfb4afb45e0bc7b", "dd44733e94f3f6237c896f2bbe9927c1eba48543", "ed90430e545529a2df7c1db6c94568ea00867a61", "3ad0af28e408092f0d12994802a9f3fe18d45f8c", "9da10a9f72e3f87e181d91b525174007a6fc4f11", "d186a0be320e6a139c42d9b018596ef9d4a0b4ca", "62b6a5dfee2e22ab9015a469cb68e4727596fd4c", "84ede759015e7480ca8e6ec2af6e2f596aa92dec", "f3085568e45c2ca74118118f792d0d55968aeb13", "84d160a3b20f1de896df0cfafe6638199d49efb8", "6915c9c6966bf1482ac93236453013535e8c5d80", "bd1236bebd1ee50c8d9206e69b1986fac9532a49", "70cff2bc010d0c047cf5b167b2c600e42f6863ab", "6610ef4a025fcea2a5958724b9493a1b081b8f66", "ca018bb3db661230fbf51bc2b3b1559ac7987040", "313e89f2da215f0dcc54a638c5749c4ee959e74a", "c0dc8a542fd18d372a2ee67a203f2cfe0a345a05", "b31c6944d9cfd596b6c25fe17e36780bfa2d7473", "3a7aecd4bcaf75c7b0b02c26ea6ceacf3e8f5f4d", "1fd80f714c85ca685a80f32e0a4e8fd3b866e310", "baf7b939ef71b25713cacbe47bef8caf80ce99c6", "118f166726472bd5b5578817503ed0992c9102e2", "4c62b2337352073ff41fa9a9857a53999d41b49b", "3addc6860c44edcf28c262489f23276b84e11812", "0df31f1cd96df8b2882b1e0faf4409b0bd704541", "a224b43863a0679f153bec24e1d329c63f1ed234", "700f71ffefd60c16bd8ce711a956967ea9071cec", "fa9a2e447e2cef4dfda40a854dd7ec35624a7799", "6d62b9b4db6228122a5f1cda81b06f156afb04ba", "14f50cd1c2f5d29d9b070746c1fcae59b68ca26b", "d3e1ce1d2b3119adf270b2d00d947beb03fe3321", "2f4525d4a200e97e1b87449c2cd9bd2e25b7e8cd", "b0b14b3d12980912723fb8b66afb48dcda742fcb", "bc28b5a964c8f5721eb58ee3f3c47a9bcbf4f4d8", "49b64e09d81c0cc84b267edd0c2fd7df5a64c78c", "8bf9683c80762d7dd47db12b68e99abea2a7ae05", "5600569133b7bdefe1daf9ec7f4abeb6d13e1786", "66a60c7201c2b8b20ce495f0295b32bb0ccbbc57", "3c13fc5715231fadb16a9b74a44d9d59c460cfa8", "c05b6b32b69d5d9144087ea0ebc6fab183fb9151", "b7ce164e9e75be4b5eb42fd89c9c53ebecea6729", "abc7bac20e8b15d5aa38d1c9af5bed4e0ffc7748", "928c299530ecce5c25dcf62f72f6aa901d6baea4", "87b2ed1c62d42fd9fbbd154095f2387c6a18f880", "67336cfb9d93779c02e1fda4c87801d352720eda", "074b9950a587f53fbdb48c3f1f84f1ece8c10592", "132630f17e198a1748f23ce33597efdf4a807fb9", "00b5ec860e174d7a2edb2b46523cdc5401513cbd", "3b17774c8087e239542afe1c7976c16c5446af26", "cc71779727e9051e59c8a242b4157fc1d3172caf", "9aab69982e4a9b91a86743f73dc48db30daf9265", "ff0cd44f590a80c5c87aaa85a0d2bab2d350bc4a", "7b6c7f676d78f988b01e9841ab18d389886ffa26", "5fcf76dda71647a65d6fafbab2bf03065bf3d52d", "c63eaf104979cae41c9c5b2ef1a9bbe5d1c05480", "a7efb5dce8081d1d96445355d55f05e6e825d41f", "89223f29832931516d6c1f00a9ef2263b8674f5a", "5506a7066998a2be47b86e28d061863a475a7ca8", "a2e83c6e6ad2086f97277540d9d9ef4aebb74a28", "da50b1b4177cbadb977d52aa70011713f37a2156", "e7d90c28cbbe26b9a31fa8a136c209418ca3c9ba", "0ce981aa4a840f84b670bbb3dfd77cd3be87ca84", "1c973b3f5c13399e1194724abe421e230c572206", "9074f509fbc3df3ad104eca5427d03eece453246", "34994ed5371f31eeaa68b294d7f729934280a733", "57001fa0b6622767e63c1b9cd2e6db666d180caa", "6a999a46cb630f44f1a77ac39213fad57a8c1492", "2c3fe5d2e5941e50947ff59c50d201d3968fac02", "1149e08b436cca632ddfe8cee39918f23b50dc6f", "0b813b7539fae6550541da8caafd6add86d4e22f", "ef65452adaf20bf7d12ef55913aba24037b82738", "b260ca7a23bb0d209771db7aae35049899433fe3", "b4ac9780b37cb1b736eae9fbcef27609b7c911ef", "86ed42574cd68662b05d3b00432a34e9a34cb12c", "a483da1de9cb174ca327059e9fd8432b0e8666b3", "6eb2c27f1b7d048a6912a42a0637e470cdc46562", "1d3f5d1fd272883cbc26f3d7fcf9ba58f66d48e0", "5204ace0d7b8410a5fb73c17a20a69e616215131", "60164baf43273401883c7c0b53b0bc4359b9e94f", "7fbf34d79ca897acf21061c2e24b607b090be1c9", "5ae5c9ec39930ae9b5a61b32b93288818ec05ec1", "f90394c695d47b16f608be5366373eec768597f1", "e0d6c62cef4929db66dd6df55bee699b2274a9cc", "fff73bb736a3ebf11974ba2ded176f16a1976f0d", "a4ad886bfdbd1a872bdb3b25a9893994b78adf11", "010245305f4faef0ed473552a58f83d281754e77", "b82b13e45d9372296362e0d6dc481f6a0f2ce0c7", "e6396ecb39ea2c91dc9901213da1d29b8ae1798c", "1ed09f94667962983cce7ec6c7a1df5c0881e08a", "ae1536faa3401b0c62f93e29fef9ffcf134a616a", "a26d3c16f32cf21cbe24c0d7dc37132c407608bb", "d716952ab58aa4369ea15126505a36544d50a333", "2949632c1b4acce0d7784f28e3152e9cf3c2ec7a", "323964c36556eb0e6209f65c1cef72b53b461ab8", "3864a1320d97d7b045f729a326e1e077661f31b7", "6f29a4f68e4156358f64f6a060c5e55ad42f5231", "e1e99c956a36e619398f9e94d775f51a85c26770", "f4d24aa8fe81caab2420dcf4cf9ceb139394b535", "f9d9e55d1072d7a697d2bf06e1847e93635a7cf9", "df7dc4df69114c694956a0ac537119527ecf1b9c", "35e36c0cafbdc3395fd4600a05c613d3073c895e", "24d091b80d513846293c00350f46d85f71797aff", "f95b25589b40b5b0965deb592445073ff3efa299", "6a93ee522c52f5bd54140b6fa0be6a503e00dc96", "657d341c197d036dc27d7f8f5b61c6ff6a678df4", "cddd306a5010eba20a133b6473d9e8d967884f57", "0d825fd2e9e4dd42ac14d5ae6c7f92cbe63de009", "8bd7794fbdaa9536354dd2d8d961d9503beb9460", "e349501d4275363646a099e1d3baed064aa2eca5", "151dbcd21c9ed6b03960a5f0b05c255c9f955618", "28b0eaf7c500c506976da8d0fc9cad6c278e8d87", "a09a8c790a20309b942a9fdbfe77da22407096e6", "2c23f53ca22d7d8885fc4522ddcadcfe7f01a783", "65935d9855ece6f85c21ad38634703d0917bf88c", "dec00ef7c6155c4ca1109ec8248f7ff58d8f6cd3", "cc3d2b7b7cb6f077e3b1ee1d3e99eb54fddfa151", "009d724771e339ff7ec6cd7c0cc170d3470904c5", "e64aea8b539905fa92fad0e7cf73ffa4375f8b32", "6c62681a2f655b49963a5983b8b0950a6120ae14", "db708f7d959dee1857ac524636e85ecf2e1781c1", "2cd0a87ff7df953f810c344bdf2fe3340b954c69", "3aab2116756442bf0d4cd1c089b24d34c3baa253", "3af797a25458550a16bf89acc8e4ab2b7f2bfce0", "235a7e571b33eda1a81e0f73a3173ef95dd020e5", "50d0390056017158bdc75c063efd5c2a898d5f0c", "4205e3cf9c44264731ad002fcd2520eb1b2bb801", "53fc648efc0c82b1e0cc806ab7abf7dbdf532273", "faa8ba85d503da4ab872d17ba8c00da0098ab2f2", "7687a145717677e64300adeb44ac29d90f844b59", "814ec05f3683b661166055a23e29dca0300cd58c", "351719631846db88eb3daf690fca5399aed3fd77", "49c100caf72d658aca8e58bd74a4ba90fa2b0d70", "8cc35f73da321c29973191f2cf143d29d26a1df7", "a3f7325c52240418c2ba257b103c3c550e140c83", "7bb85ce2cf23af5b2d7467c2825fa2d0330ec5d5", "6fe2e3bb57daebd1555494818909f9664376dd6c", "1c63879e1f630e44ad8d2245b8a28a088f387e7c", "313913e603eaf3bb2c3b05079046ec07bb61f8c6", "4f062ad1aebb1255b84c851d00694cc7949de832", "887697058d8464462e8fd6d23c8461e90aec8c08", "2ab94758b0276a8a26102adf8d528cf6d0567b9a", "5397c9a02f77744da25d4ef63a7ebf01affeca62", "d6adb54fefe72482ed049f07af31ddf2c287345f", "4c65b7b43f3fe31350f74cb7d0b2461e111e8dd0", "e6feb6b7c06600924e8b6bda3263c870cfb0a447", "a09d2c48d3285f206fafbffe0e50619284e92126", "925720c5d40c4ebf8601e06025e1402251ef71d2", "611b82d4c4b4f67cc3d83cf0697ec660fcee2fff", "dfd5101b17da36c32ae024b984e0b72712f01a35", "68f1af10052713fda01bfb1e5b831dcf6d826ab2", "e2133b723d0e42be74880d34de6bf6538ea7f915", "e40429d9dd849c5fe0bdf97062b1d9358d99826d", "0ac2d2817d649e3203a8f7c93e7c65be0ca9662e", "7ef25e94db74d85fa7e9271b064a3c7d9ef7add5", "3e05dcce371d3f672feba29f086ad78a93ae3996", "16b9f8ab972e67eb21872ea2c40046249d543989", "c47579857bbf12c85499f431d4ecf27d77976b7c", "1ea4bec1a921180164852c65006d928617bd2caf", "d3ebf0f291297649b4c8dc3ecc81d2eddedc100d", "0ddae73613ab823639de096c287ea6142749f340", "6638e37b887b5a279044afbdc9928e19f678eb2e", "de7b8a41bbe1ccdfc009de51fa6d160db3ca8025", "f52de0603f31798455e48bd90e10a8f888dd6d93", } for i := 0; i < 5; i++ { t.Logf("Iteration %d", i+1) for _, sha := range shas { pkg, err := ms.GetMavenPackageBySha(ctx, sha) if err != nil { if strings.Contains(err.Error(), "no artifact found") { t.Logf("failed to get package by sha: %v", err) continue } else { t.Fatalf("failed to get package by sha: %v", err) } } // log human readable timestamp ti := time.Now() t.Logf("Time: %s Success: %s:%s", ti.String(), pkg.Name, pkg.Version) } } } ================================================ FILE: grype/matcher/java/matcher_mocks_test.go ================================================ package java import ( "context" "errors" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" syftPkg "github.com/anchore/syft/syft/pkg" ) func newMockProvider() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { PackageName: "org.springframework.spring-webmvc", Constraint: version.MustGetConstraint(">=5.0.0,<5.1.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "github:language:" + syftPkg.Java.String()}, }, { PackageName: "org.springframework.spring-webmvc", Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "github:language:" + syftPkg.Java.String()}, }, // Package name is expected to resolve to : if pom groupID and artifactID is present // See JavaResolver.Names: https://github.com/anchore/grype/blob/402067e958a4fa9d20384752351d6c54b0436ba1/grype/db/v6/name/java.go#L19 { PackageName: "org.springframework:spring-webmvc", Constraint: version.MustGetConstraint(">=5.0.0,<5.1.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "github:language:" + syftPkg.Java.String()}, }, { PackageName: "org.springframework:spring-webmvc", Constraint: version.MustGetConstraint(">=5.0.1,<5.1.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: "github:language:" + syftPkg.Java.String()}, }, // unexpected... { PackageName: "org.springframework.spring-webmvc", Constraint: version.MustGetConstraint(">=5.0.0,<5.0.7", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: "github:language:" + syftPkg.Java.String()}, }, }...) } type mockMavenSearcher struct { pkg pkg.Package simulateRateLimiting bool } func (m mockMavenSearcher) GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) { if m.simulateRateLimiting { return nil, errors.New("you been rate limited") } return &m.pkg, nil } ================================================ FILE: grype/matcher/java/matcher_test.go ================================================ package java import ( "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) { newMatcher := func(searcher MavenSearcher) *Matcher { return &Matcher{ cfg: MatcherConfig{ ExternalSearchConfig: ExternalSearchConfig{ SearchMavenUpstream: true, }, }, MavenSearcher: searcher, } } store := newMockProvider() // Define test cases testCases := []struct { testname string testExpectRateLimit bool packages []pkg.Package }{ { testname: "do not search maven - metadata present", testExpectRateLimit: false, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "spring-webmvc", PomGroupID: "org.springframework", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }, }, }, }, }, }, { testname: "search maven - missing metadata", testExpectRateLimit: false, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "", PomGroupID: "", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }, }, }, }, }, }, { testname: "search maven - missing sha1 error", testExpectRateLimit: false, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "", PomGroupID: "", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "", }, }, }, }, }, }, } t.Run("matching from maven search results", func(t *testing.T) { for _, p := range testCases { // Adding test isolation t.Run(p.testname, func(t *testing.T) { matcher := newMatcher(mockMavenSearcher{ pkg: p.packages[0], }) actual, _ := matcher.matchUpstreamMavenPackages(store, p.packages[0]) assert.Len(t, actual, 2, "unexpected matches count") foundCVEs := stringutil.NewStringSet() for _, v := range actual { foundCVEs.Add(v.Vulnerability.ID) require.NotEmpty(t, v.Details) for _, d := range v.Details { assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated") assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") } assert.Equal(t, p.packages[0].Name, v.Package.Name, "failed to capture original package name") } for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} { if !foundCVEs.Contains(id) { t.Errorf("missing discovered CVE: %s", id) } } if t.Failed() { t.Logf("discovered CVES: %+v", foundCVEs) } }) } }) t.Run("handles maven rate limiting", func(t *testing.T) { for _, p := range testCases { // Adding test isolation t.Run(p.testname, func(t *testing.T) { matcher := newMatcher(mockMavenSearcher{simulateRateLimiting: true}) _, err := matcher.matchUpstreamMavenPackages(store, p.packages[0]) if p.testExpectRateLimit { assert.Errorf(t, err, "should have gotten an error from the rate limiting") } }) } }) } func TestMatcherJava_shouldSearchMavenBySha(t *testing.T) { newMatcher := func(searcher MavenSearcher) *Matcher { return &Matcher{ cfg: MatcherConfig{ ExternalSearchConfig: ExternalSearchConfig{ SearchMavenUpstream: true, }, }, MavenSearcher: searcher, } } // Define test cases testCases := []struct { testname string expectedShouldSearchMaven bool testExpectedError bool packages []pkg.Package }{ { testname: "do not search maven - metadata present", expectedShouldSearchMaven: false, testExpectedError: false, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "spring-webmvc", PomGroupID: "org.springframework", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }, }, }, }, }, }, { testname: "search maven - missing metadata", expectedShouldSearchMaven: true, testExpectedError: false, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "", PomGroupID: "", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }, }, }, }, }, }, { testname: "search maven - missing artifactId", expectedShouldSearchMaven: true, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "", PomGroupID: "org.springframework", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }, }, }, }, }, }, { testname: "do not search maven - missing sha1", expectedShouldSearchMaven: false, packages: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "org.springframework.spring-webmvc", Version: "5.1.5.RELEASE", Language: syftPkg.Java, Type: syftPkg.JavaPkg, Metadata: pkg.JavaMetadata{ PomArtifactID: "", PomGroupID: "", ArchiveDigests: []pkg.Digest{ { Algorithm: "sha1", Value: "", }, }, }, }, }, }, } t.Run("matching from Maven search results", func(t *testing.T) { for _, p := range testCases { // Adding test isolation t.Run(p.testname, func(t *testing.T) { matcher := newMatcher(mockMavenSearcher{ pkg: p.packages[0], }) actual, digests := matcher.shouldSearchMavenBySha(p.packages[0]) assert.Equal(t, p.expectedShouldSearchMaven, actual, "unexpected decision to search Maven") if actual { assert.NotEmpty(t, digests, "sha digests should not be empty when search is expected") } }) } }) } ================================================ FILE: grype/matcher/java/maven_search.go ================================================ package java import ( "context" "encoding/json" "errors" "fmt" "net/http" "sort" "time" "golang.org/x/time/rate" "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" ) // MavenSearcher is the interface that wraps the GetMavenPackageBySha method. type MavenSearcher interface { // GetMavenPackageBySha provides an interface for building a package from maven data based on a sha1 digest GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) } // mavenSearch implements the MavenSearcher interface type mavenSearch struct { client *http.Client baseURL string rateLimiter *rate.Limiter } // newMavenSearch creates a new mavenSearch instance with rate limiting // rate is specified as 1 request per 300ms func newMavenSearch(client *http.Client, baseURL string, rateLimit time.Duration) *mavenSearch { return &mavenSearch{ client: client, baseURL: baseURL, rateLimiter: rate.NewLimiter(rate.Every(rateLimit), 1), } } type mavenAPIResponse struct { Response struct { NumFound int `json:"numFound"` Docs []struct { ID string `json:"id"` GroupID string `json:"g"` ArtifactID string `json:"a"` Version string `json:"v"` P string `json:"p"` VersionCount int `json:"versionCount"` } `json:"docs"` } `json:"response"` } func (ms *mavenSearch) GetMavenPackageBySha(ctx context.Context, sha1 string) (*pkg.Package, error) { if sha1 == "" { return nil, errors.New("empty sha1 digest") } if ms.baseURL == "" { return nil, errors.New("empty maven search URL") } if ms.rateLimiter == nil { return nil, errors.New("rate limiter not initialized") } if ms.client == nil { return nil, errors.New("HTTP client not initialized") } // Wait for rate limiter err := ms.rateLimiter.Wait(ctx) if err != nil { return nil, fmt.Errorf("rate limiter error: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, ms.baseURL, nil) if err != nil { return nil, fmt.Errorf("unable to initialize HTTP client: %w", err) } q := req.URL.Query() q.Set("q", fmt.Sprintf(sha1Query, sha1)) q.Set("rows", "1") q.Set("wt", "json") req.URL.RawQuery = q.Encode() resp, err := ms.client.Do(req) if err != nil { return nil, fmt.Errorf("sha1 search error: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("status %s from %s", resp.Status, req.URL.String()) } var res mavenAPIResponse if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil, fmt.Errorf("json decode error: %w", err) } if len(res.Response.Docs) == 0 { return nil, fmt.Errorf("digest %s: %w", sha1, errors.New("no artifact found")) } // artifacts might have the same SHA-1 digests. // e.g. "javax.servlet:jstl" and "jstl:jstl" docs := res.Response.Docs sort.Slice(docs, func(i, j int) bool { return docs[i].ID < docs[j].ID }) d := docs[0] return &pkg.Package{ Name: fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID), Version: d.Version, Language: syftPkg.Java, Metadata: pkg.JavaMetadata{ PomArtifactID: d.ArtifactID, PomGroupID: d.GroupID, }, }, nil } ================================================ FILE: grype/matcher/java/maven_test.go ================================================ package java import ( "context" "net/http" "net/http/httptest" "testing" "time" "golang.org/x/time/rate" ) func TestNewMavenSearchRateLimiter(t *testing.T) { // Create a test server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // We don't need to respond with anything for this test })) defer ts.Close() t.Run("custom rate limit initialization", func(t *testing.T) { customDuration := 500 * time.Millisecond ms := newMavenSearch(http.DefaultClient, ts.URL, customDuration) expectedRate := rate.Every(customDuration) if ms.rateLimiter.Limit() != expectedRate { t.Errorf("unexpected rate limit: got %v, want %v", ms.rateLimiter.Limit(), rate.Limit(expectedRate)) } }) t.Run("default rate limit initialization", func(t *testing.T) { defaultDuration := 300 * time.Millisecond ms := newMavenSearch(http.DefaultClient, ts.URL, defaultDuration) expectedRate := rate.Every(defaultDuration) if ms.rateLimiter.Limit() != expectedRate { t.Errorf("unexpected rate limit: got %v, want %v", ms.rateLimiter.Limit(), rate.Limit(expectedRate)) } }) t.Run("rate limiter behavior", func(t *testing.T) { ms := newMavenSearch(http.DefaultClient, ts.URL, 200*time.Millisecond) ctx := context.Background() // First request should proceed immediately start := time.Now() err := ms.rateLimiter.Wait(ctx) if err != nil { t.Errorf("unexpected error on first wait: %v", err) } if elapsed := time.Since(start); elapsed > 50*time.Millisecond { t.Errorf("first request took too long: %v", elapsed) } // Second request should be delayed start = time.Now() err = ms.rateLimiter.Wait(ctx) if err != nil { t.Errorf("unexpected error on second wait: %v", err) } if elapsed := time.Since(start); elapsed < 150*time.Millisecond { t.Errorf("rate limiting not enforced, second request took: %v", elapsed) } }) t.Run("config integration", func(t *testing.T) { testCases := []struct { name string rateLimit time.Duration want rate.Limit }{ { name: "with default rate limit", rateLimit: 300 * time.Millisecond, want: rate.Every(300 * time.Millisecond), }, { name: "with custom rate limit", rateLimit: 500 * time.Millisecond, want: rate.Every(500 * time.Millisecond), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ms := newMavenSearch(http.DefaultClient, ts.URL, tc.rateLimit) if ms.rateLimiter.Limit() != tc.want { t.Errorf("rate limit = %v, want %v", ms.rateLimiter.Limit(), tc.want) } }) } }) } func withinDelta(got, want, delta time.Duration) bool { diff := got - want if diff < 0 { diff = -diff } return diff <= delta } ================================================ FILE: grype/matcher/javascript/matcher.go ================================================ package javascript import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewJavascriptMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.NpmPkg} } func (m *Matcher) Type() match.MatcherType { return match.JavascriptMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/matchers.go ================================================ package matcher import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/apk" "github.com/anchore/grype/grype/matcher/bitnami" "github.com/anchore/grype/grype/matcher/dotnet" "github.com/anchore/grype/grype/matcher/dpkg" "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/hex" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" "github.com/anchore/grype/grype/matcher/msrc" "github.com/anchore/grype/grype/matcher/pacman" "github.com/anchore/grype/grype/matcher/portage" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/rpm" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/rust" "github.com/anchore/grype/grype/matcher/stock" ) // Config contains values used by individual matcher structs for advanced configuration type Config struct { Java java.MatcherConfig Ruby ruby.MatcherConfig Python python.MatcherConfig Dotnet dotnet.MatcherConfig Javascript javascript.MatcherConfig Golang golang.MatcherConfig Rust rust.MatcherConfig Hex hex.MatcherConfig Stock stock.MatcherConfig Dpkg dpkg.MatcherConfig Rpm rpm.MatcherConfig } func NewDefaultMatchers(mc Config) []match.Matcher { return []match.Matcher{ dpkg.NewDpkgMatcher(mc.Dpkg), ruby.NewRubyMatcher(mc.Ruby), python.NewPythonMatcher(mc.Python), dotnet.NewDotnetMatcher(mc.Dotnet), rpm.NewRpmMatcher(mc.Rpm), java.NewJavaMatcher(mc.Java), javascript.NewJavascriptMatcher(mc.Javascript), &apk.Matcher{}, golang.NewGolangMatcher(mc.Golang), &msrc.Matcher{}, &portage.Matcher{}, rust.NewRustMatcher(mc.Rust), hex.NewHexMatcher(mc.Hex), stock.NewStockMatcher(mc.Stock), &bitnami.Matcher{}, &pacman.Matcher{}, } } ================================================ FILE: grype/matcher/mock/matcher.go ================================================ package mock import ( "errors" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) // MatchFunc is a function that takes a vulnerability provider and a package, // and returns matches, ignored matches, and an error. type MatchFunc func(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) // Matcher is a mock implementation of the match.Matcher interface. This is // intended for testing purposes only. type Matcher struct { typ syftPkg.Type matchFunc MatchFunc } // New creates a new mock Matcher with the given type and match function. func New(typ syftPkg.Type, matchFunc MatchFunc) *Matcher { return &Matcher{ typ: typ, matchFunc: matchFunc, } } func (m Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{m.typ} } func (m Matcher) Type() match.MatcherType { return "MOCK" } func (m Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { if m.matchFunc != nil { return m.matchFunc(vp, p) } return nil, nil, errors.New("no match function provided") } ================================================ FILE: grype/matcher/msrc/matcher.go ================================================ package msrc import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { } func (m *Matcher) PackageTypes() []syftPkg.Type { // This looks like there is a special package, but in reality, this is just // a workaround. MSRC matching is done at the KB-patch level, and so this // treats KBs as "packages" but they aren't packages, they are patches return []syftPkg.Type{syftPkg.KbPkg} } func (m *Matcher) Type() match.MatcherType { return match.MsrcMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { // find KB matches for the MSFT version given in the package and version. // The "package" holds the information about the Windows version, and its // patch (KB) return internal.MatchPackageByEcosystemPackageName(store, p, p.Name, m.Type()) } ================================================ FILE: grype/matcher/pacman/matcher.go ================================================ package pacman import ( "fmt" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct{} func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.AlpmPkg} } func (m *Matcher) Type() match.MatcherType { return match.PacmanMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { matches, ignoreFilters, err := internal.MatchPackageByDistro(store, p, nil, m.Type(), nil) if err != nil { return nil, nil, fmt.Errorf("failed to match pacman package: %w", err) } return matches, ignoreFilters, nil } ================================================ FILE: grype/matcher/pacman/matcher_test.go ================================================ package pacman import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcherType(t *testing.T) { m := Matcher{} assert.Equal(t, match.PacmanMatcher, m.Type()) } func TestMatcherPackageTypes(t *testing.T) { m := Matcher{} assert.Equal(t, []syftPkg.Type{syftPkg.AlpmPkg}, m.PackageTypes()) } func TestMatch(t *testing.T) { archVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "AVG-1234", Namespace: "arch:distro:archlinux:rolling", }, PackageName: "curl", Constraint: version.MustGetConstraint("< 8.5.0-1", version.PacmanFormat), } vp := mock.VulnerabilityProvider(archVuln) m := Matcher{} d := distro.New(distro.ArchLinux, "", "rolling") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "curl", Version: "8.4.0-1", Type: syftPkg.AlpmPkg, Distro: d, } expected := []match.Match{ { Vulnerability: archVuln, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: d.Type.String(), Version: d.Version, }, Package: match.PackageParameter{ Name: "curl", Version: "8.4.0-1", }, Namespace: "arch:distro:archlinux:rolling", }, Found: match.DistroResult{ VulnerabilityID: "AVG-1234", VersionConstraint: archVuln.Constraint.String(), }, Matcher: match.PacmanMatcher, }, }, }, } actual, _, err := m.Match(vp, p) require.NoError(t, err) assertMatches(t, expected, actual) } func TestMatchNoVulnerability(t *testing.T) { archVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "AVG-1234", Namespace: "arch:distro:archlinux:rolling", }, PackageName: "curl", Constraint: version.MustGetConstraint("< 8.0.0-1", version.PacmanFormat), } vp := mock.VulnerabilityProvider(archVuln) m := Matcher{} d := distro.New(distro.ArchLinux, "", "rolling") // Package version is newer than the constraint, should not match p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "curl", Version: "8.5.0-1", Type: syftPkg.AlpmPkg, Distro: d, } actual, _, err := m.Match(vp, p) require.NoError(t, err) assert.Empty(t, actual) } func TestMatchWithEpoch(t *testing.T) { archVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "AVG-5678", Namespace: "arch:distro:archlinux:rolling", }, PackageName: "openssl", Constraint: version.MustGetConstraint("< 1:3.0.8-1", version.PacmanFormat), } vp := mock.VulnerabilityProvider(archVuln) m := Matcher{} d := distro.New(distro.ArchLinux, "", "rolling") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "openssl", Version: "1:3.0.7-4", Type: syftPkg.AlpmPkg, Distro: d, } actual, _, err := m.Match(vp, p) require.NoError(t, err) require.Len(t, actual, 1) assert.Equal(t, "AVG-5678", actual[0].Vulnerability.ID) } func TestMatchNilDistro(t *testing.T) { m := Matcher{} p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "curl", Version: "8.4.0-1", Type: syftPkg.AlpmPkg, Distro: nil, } actual, _, err := m.Match(mock.VulnerabilityProvider(), p) require.NoError(t, err) assert.Empty(t, actual) } func assertMatches(t *testing.T, expected, actual []match.Match) { t.Helper() opts := []cmp.Option{ cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.IgnoreFields(pkg.Package{}, "Locations"), cmpopts.IgnoreUnexported(distro.Distro{}), } if diff := cmp.Diff(expected, actual, opts...); diff != "" { t.Errorf("mismatch (-want +got):\n%s", diff) } } ================================================ FILE: grype/matcher/portage/matcher.go ================================================ package portage import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.PortagePkg} } func (m *Matcher) Type() match.MatcherType { return match.PortageMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { // Portage doesn't use epochs, so pass nil for the config return internal.MatchPackageByDistro(store, p, nil, m.Type(), nil) } ================================================ FILE: grype/matcher/portage/matcher_mocks_test.go ================================================ package portage import ( "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" ) func newMockProvider() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ // direct... { PackageName: "app-misc/neutron", Constraint: version.MustGetConstraint("< 2014.1.3", version.PortageFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: "secdb:distro:gentoo:"}, }, { PackageName: "app-misc/neutron", Constraint: version.MustGetConstraint("< 2014.1.4", version.PortageFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: "secdb:distro:gentoo:"}, }, }...) } ================================================ FILE: grype/matcher/portage/matcher_test.go ================================================ package portage import ( "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcherPortage_Match(t *testing.T) { matcher := Matcher{} d := distro.New(distro.Gentoo, "", "") p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "app-misc/neutron", Version: "2014.1.3", Type: syftPkg.PortagePkg, Distro: d, } store := newMockProvider() actual, _, err := matcher.Match(store, p) assert.NoError(t, err, "unexpected err from Match", err) assert.Len(t, actual, 1, "unexpected indirect matches count") foundCVEs := stringutil.NewStringSet() for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) require.NotEmpty(t, a.Details) assert.Equal(t, p.Name, a.Package.Name, "failed to capture original package name") for _, detail := range a.Details { assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type") } } for _, id := range []string{"CVE-2014-fake-2"} { if !foundCVEs.Contains(id) { t.Errorf("missing discovered CVE: %s", id) } } if t.Failed() { t.Logf("discovered CVES: %+v", foundCVEs) } } ================================================ FILE: grype/matcher/python/matcher.go ================================================ package python import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewPythonMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.PythonPkg} } func (m *Matcher) Type() match.MatcherType { return match.PythonMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/rpm/almalinux.go ================================================ package rpm import ( "fmt" "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) // shouldUseAlmaLinuxMatching determines if AlmaLinux-specific matching should be used func shouldUseAlmaLinuxMatching(d *distro.Distro) bool { if d == nil { return false } return d.Type == distro.AlmaLinux } // markAsIndirectMatches updates all match details in the result set to indicate indirect matches func markAsIndirectMatches(results result.Set) result.Set { updated := make(result.Set) for key, resultList := range results { var updatedResults []result.Result for _, r := range resultList { // Update all Details to ExactIndirectMatch updatedDetails := make([]match.Detail, len(r.Details)) for i, detail := range r.Details { updatedDetails[i] = detail updatedDetails[i].Type = match.ExactIndirectMatch } r.Details = updatedDetails updatedResults = append(updatedResults, r) } updated[key] = updatedResults } return updated } // almaLinuxMatchesWithUpstreams handles AlmaLinux matching for both the binary package and its upstream packages // This function orchestrates the complete AlmaLinux matching flow: // 1. Search for RHEL disclosures for the binary package // 2. Search for RHEL disclosures for all upstream (source) packages // 3. Search for AlmaLinux unaffected records for the binary package and related packages // 4. Apply filtering logic to determine which disclosures are still vulnerable on AlmaLinux func almaLinuxMatchesWithUpstreams(provider result.Provider, binaryPkg pkg.Package) ([]match.Match, error) { if strings.HasSuffix(binaryPkg.Name, "-debuginfo") || strings.HasSuffix(binaryPkg.Name, "-debugsource") { return nil, nil // almalinux explicitly never publishes advisories for RPMs that are only debug material } // Create a RHEL-compatible distro for finding base disclosures rhelCompatibleDistro := *binaryPkg.Distro rhelCompatibleDistro.Type = distro.RedHat // treat as RHEL for disclosure lookup pkgVersion := version.New(binaryPkg.Version, pkg.VersionFormat(binaryPkg)) // Step 1: Find RHEL disclosures for the binary package (direct match) binaryDisclosures, err := provider.FindResults( search.ByPackageName(binaryPkg.Name), search.ByDistro(rhelCompatibleDistro), internal.OnlyQualifiedPackages(binaryPkg), internal.OnlyVulnerableVersions(pkgVersion), ) if err != nil { return nil, fmt.Errorf("matcher failed to fetch RHEL disclosures for AlmaLinux binary pkg=%q: %w", binaryPkg.Name, err) } // Step 2: Find RHEL disclosures for upstream (source) packages (indirect match) // Note: We do NOT add epochs to upstream package versions because sourceRPMs often omit epochs // even when the source package has a non-zero epoch. See the comment in matchUpstreamPackages // in matcher.go for the full explanation of why this is necessary. upstreamDisclosures := result.Set{} for _, upstreamPkg := range pkg.UpstreamPackages(binaryPkg) { // Create a version object from the upstream package WITHOUT adding epoch // This avoids false positives where binary package epochs differ from source package epochs upstreamVersion := version.New(upstreamPkg.Version, pkg.VersionFormat(upstreamPkg)) upstreamResults, err := provider.FindResults( search.ByPackageName(upstreamPkg.Name), search.ByDistro(rhelCompatibleDistro), internal.OnlyQualifiedPackages(upstreamPkg), internal.OnlyVulnerableVersions(upstreamVersion), ) if err != nil { log.WithFields("error", err, "upstreamPkg", upstreamPkg.Name, "binaryPkg", binaryPkg.Name).Debug("failed to fetch RHEL disclosures for upstream package") continue } // Mark these as indirect matches since they came from upstream package search upstreamResults = markAsIndirectMatches(upstreamResults) upstreamDisclosures = upstreamDisclosures.Merge(upstreamResults) } // Merge all disclosures (binary + upstream) allDisclosures := binaryDisclosures.Merge(upstreamDisclosures) if len(allDisclosures) == 0 { return nil, nil } // Step 3: Find AlmaLinux unaffected records for the binary package directUnaffected, err := provider.FindResults( search.ByPackageName(binaryPkg.Name), search.ByExactDistro(*binaryPkg.Distro), // use exact AlmaLinux distro for unaffected lookup (no aliases) internal.OnlyQualifiedPackages(binaryPkg), search.ForUnaffected(), ) if err != nil { log.WithFields("error", err, "distro", binaryPkg.Distro, "pkg", binaryPkg.Name).Debug("failed to fetch AlmaLinux unaffected packages") // If we can't get unaffected data, return the original disclosures return allDisclosures.ToMatches(), nil } // Step 4: Find AlmaLinux unaffected records for related packages (source/binary relationships) // This handles cases where AlmaLinux publishes unaffected records for binary packages (e.g., python3-tkinter) // but the disclosure is for the source package (e.g., python3) relatedUnaffected := findRelatedUnaffectedPackages(provider, binaryPkg) // Merge all unaffected results allUnaffected := directUnaffected.Merge(relatedUnaffected) // Step 5: Apply filtering logic: if disclosure exists and no fix applies, the package is vulnerable updatedDisclosures := applyAlmaLinuxUnaffectedFiltering(allDisclosures, allUnaffected, pkgVersion) return updatedDisclosures.ToMatches(), nil } // findRelatedUnaffectedPackages searches for unaffected packages using source/binary RPM relationships func findRelatedUnaffectedPackages(provider result.Provider, searchPkg pkg.Package) result.Set { allResults := make(result.Set) // Get all related package names (source RPM, binary RPM patterns, etc.) relatedNames := getRelatedPackageNames(searchPkg) for _, relatedName := range relatedNames { if relatedName == searchPkg.Name { continue // skip the main package name as it's already searched } // Search for unaffected records using related package names relatedResults, err := provider.FindResults( search.ByPackageName(relatedName), search.ByExactDistro(*searchPkg.Distro), // use exact distro to avoid alias mapping internal.OnlyQualifiedPackages(searchPkg), search.ForUnaffected(), ) if err != nil { log.WithFields("error", err, "relatedName", relatedName, "originalPkg", searchPkg.Name).Debug("failed to fetch related unaffected packages") continue } if len(relatedResults) > 0 { log.WithFields("relatedName", relatedName, "originalPkg", searchPkg.Name, "foundUnaffected", len(relatedResults)).Trace("found unaffected records via package relationship") // Merge results into our set for key, results := range relatedResults { allResults[key] = append(allResults[key], results...) } } } return allResults } // applyAlmaLinuxUnaffectedFiltering applies AlmaLinux unaffected filtering and fix updates func applyAlmaLinuxUnaffectedFiltering(disclosures result.Set, unaffectedResults result.Set, pkgVersion *version.Version) result.Set { if len(unaffectedResults) == 0 { return disclosures } // Filter out vulnerabilities where package version satisfies the unaffected constraint // (i.e., package IS safe according to AlmaLinux) filtered := disclosures.Remove( unaffectedResults.Filter(search.ByVersion(*pkgVersion)), ) // Update remaining vulnerabilities with AlmaLinux fix information return filtered.Update(unaffectedResults, result.IdentitiesOverlap, replaceWithAlmaLinuxFixInfo) } // replaceWithAlmaLinuxFixInfo updates the Constraint, Fix, and Advisories fields from AlmaLinux unaffected data // while preserving the match Details from the RHEL disclosure. This is used to replace RHEL fix // versions with AlmaLinux-specific fix versions when available. func replaceWithAlmaLinuxFixInfo(existing *result.Result, incoming result.Result) { // For each vulnerability in the existing result (RHEL disclosure) for i := range existing.Vulnerabilities { // Find the corresponding AlmaLinux vulnerability and extract fix info for _, incomingVuln := range incoming.Vulnerabilities { // Extract fix version from the unaffected constraint (e.g., ">= 2.4.48" -> "2.4.48") fixVersion := extractFixVersionFromConstraint(incomingVuln.Constraint) if fixVersion == "" { continue } // Update constraint to match AlmaLinux fix version (form: "< fixVersion") newConstraint, err := version.GetConstraint(fmt.Sprintf("< %s", fixVersion), version.RpmFormat) if err != nil { log.WithFields("error", err, "fixVersion", fixVersion).Debug("failed to create constraint from AlmaLinux fix version") continue } existing.Vulnerabilities[i].Constraint = newConstraint // Update fix version and advisories to AlmaLinux's data existing.Vulnerabilities[i].Fix = vulnerability.Fix{ Versions: []string{fixVersion}, State: vulnerability.FixStateFixed, } // Use advisories from database, or construct if missing advisories := incomingVuln.Advisories if len(advisories) == 0 { advisories = constructAdvisory(incomingVuln, existing.Package) } existing.Vulnerabilities[i].Advisories = advisories // Update Details to reflect the AlmaLinux constraint in the match details for j := range existing.Details { if distroResult, ok := existing.Details[j].Found.(match.DistroResult); ok { distroResult.VersionConstraint = newConstraint.String() existing.Details[j].Found = distroResult } } break // Only need first match } } } // constructAdvisory builds advisory information from an ALSA vulnerability // This is a fallback for databases that don't yet have advisory IDs in fix references. // Once grype-db is updated to include advisory IDs, this will no longer be needed. func constructAdvisory(vuln vulnerability.Vulnerability, pkg *pkg.Package) []vulnerability.Advisory { // Only construct for ALSA advisories if !strings.HasPrefix(vuln.ID, "ALSA-") { return nil } // Extract major version from package distro if pkg == nil || pkg.Distro == nil { return nil } majorVersion := pkg.Distro.Version if idx := strings.Index(majorVersion, "."); idx != -1 { majorVersion = majorVersion[:idx] } if majorVersion == "" { return nil } // Format: ALSA-YYYY:NNNN -> https://errata.almalinux.org/{major}/ALSA-YYYY-NNNN.html alsaURLID := strings.Replace(vuln.ID, ":", "-", 1) // ALSA-2025:2686 -> ALSA-2025-2686 return []vulnerability.Advisory{ { ID: vuln.ID, Link: fmt.Sprintf("https://errata.almalinux.org/%s/%s.html", majorVersion, alsaURLID), }, } } // extractFixVersionFromConstraint extracts a fix version from a version constraint // e.g., ">= 2.4.48" → "2.4.48", "= 1.2.3-4.el8" → "1.2.3-4.el8" func extractFixVersionFromConstraint(constraint version.Constraint) string { if constraint == nil { return "" } // constraint.Value() returns raw constraint without format suffix (e.g., ">= 2.4.48" instead of ">= 2.4.48 (rpm)") constraintStr := constraint.Value() // Handle common constraint patterns // ">= version" → "version" if strings.HasPrefix(constraintStr, ">=") { return strings.TrimSpace(strings.TrimPrefix(constraintStr, ">=")) } // "= version" → "version" if strings.HasPrefix(constraintStr, "= ") { return strings.TrimPrefix(constraintStr, "= ") } // "> version" → we can't determine exact fix version, return empty // "< version" → this wouldn't make sense for a fix constraint return "" } ================================================ FILE: grype/matcher/rpm/almalinux_package_utils.go ================================================ package rpm import ( "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" ) // extractSourceRPMName extracts the source RPM package name from an RPM package. // For binary RPMs, this returns the source package name they were built from. // For source RPMs, this returns the package name itself. // For non-RPM packages, returns empty string. func extractSourceRPMName(p pkg.Package) string { // Only process RPM packages if p.Type != syftPkg.RpmPkg { return "" } // First, check if this package has upstream information (source RPM) for _, upstream := range p.Upstreams { if upstream.Name != "" && upstream.Name != p.Name { return upstream.Name } } // If no upstream info, return the package name itself return p.Name } // getRelatedPackageNames returns all possible package names that could be related to the given package. // This includes: // 1. The package name itself // 2. Source RPM name (if this is a binary package) func getRelatedPackageNames(p pkg.Package) []string { names := []string{p.Name} // Add source RPM name if different from package name sourceRPMName := extractSourceRPMName(p) if sourceRPMName != "" && sourceRPMName != p.Name { names = append(names, sourceRPMName) } return names } ================================================ FILE: grype/matcher/rpm/almalinux_package_utils_test.go ================================================ package rpm import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/pkg" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestExtractSourceRPMName(t *testing.T) { tests := []struct { name string pkg pkg.Package expected string }{ { name: "binary package with upstream source", pkg: pkg.Package{ Name: "python3-criu", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "criu", Version: "3.12"}, }, }, expected: "criu", }, { name: "source package with no upstreams", pkg: pkg.Package{ Name: "criu", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{}, }, expected: "criu", }, { name: "package with self-referential upstream", pkg: pkg.Package{ Name: "kernel", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "kernel", Version: "5.4.0"}, }, }, expected: "kernel", }, { name: "package with RPM metadata but no upstreams", pkg: pkg.Package{ Name: "util-linux", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, expected: "util-linux", }, { name: "non-RPM package", pkg: pkg.Package{ Name: "some-deb-package", Type: syftPkg.DebPkg, }, expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractSourceRPMName(tt.pkg) assert.Equal(t, tt.expected, result) }) } } func TestGetRelatedPackageNames(t *testing.T) { tests := []struct { name string pkg pkg.Package expected []string }{ { name: "binary package with source upstream", pkg: pkg.Package{ Name: "python3-criu", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "criu", Version: "3.12"}, }, }, expected: []string{"python3-criu", "criu"}, // should include both binary and source names }, { name: "source package with no upstreams", pkg: pkg.Package{ Name: "httpd", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{}, }, expected: []string{"httpd"}, // should only include itself }, { name: "package with self-referential upstream", pkg: pkg.Package{ Name: "kernel", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ {Name: "kernel", Version: "5.4.0"}, }, }, expected: []string{"kernel"}, // should only include itself since source name is same }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getRelatedPackageNames(tt.pkg) // Check that all expected names are present for _, expected := range tt.expected { assert.Contains(t, result, expected, "Missing expected package name: %s", expected) } // Check that we don't have unexpected names assert.Equal(t, len(tt.expected), len(result), "Unexpected number of package names") // The first name should always be the package name itself require.NotEmpty(t, result) assert.Equal(t, tt.pkg.Name, result[0]) }) } } // Helper function for tests func intPtr(i int) *int { return &i } ================================================ FILE: grype/matcher/rpm/almalinux_test.go ================================================ package rpm import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" ) // MockProvider is a simple mock implementation of result.Provider for testing type MockProvider struct { results map[string]result.Set findResultsFunc func(criteria ...vulnerability.Criteria) (result.Set, error) } func (m *MockProvider) FindResults(criteria ...vulnerability.Criteria) (result.Set, error) { if m.findResultsFunc != nil { return m.findResultsFunc(criteria...) } // Default behavior - return empty set return result.Set{}, nil } func TestShouldUseAlmaLinuxMatching(t *testing.T) { tests := []struct { name string distro *distro.Distro expected bool }{ { name: "nil distro", distro: nil, expected: false, }, { name: "AlmaLinux distro", distro: &distro.Distro{ Type: distro.AlmaLinux, }, expected: true, }, { name: "RHEL distro", distro: &distro.Distro{ Type: distro.RedHat, }, expected: false, }, { name: "Ubuntu distro", distro: &distro.Distro{ Type: distro.Ubuntu, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := shouldUseAlmaLinuxMatching(tt.distro) assert.Equal(t, tt.expected, result) }) } } // this should be called "TestModularityQualifiersPassedToVulnProvider" func TestModularityExcludesDisclosure(t *testing.T) { // Test that OnlyQualifiedPackages is used to filter both RHEL disclosures and AlmaLinux advisories mockProvider := &MockProvider{} almaDistro := &distro.Distro{ Type: distro.AlmaLinux, Version: "8", } testPkg := pkg.Package{ Name: "nodejs", Version: "1:20.8.0-1.module_el8.9.0+3775+d8460d29", Type: syftPkg.RpmPkg, Distro: almaDistro, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("nodejs:20"), }, } var capturedCriteria [][]vulnerability.Criteria callCount := 0 mockProvider.findResultsFunc = func(criteria ...vulnerability.Criteria) (result.Set, error) { capturedCriteria = append(capturedCriteria, criteria) callCount++ // First call: return a disclosure so the matcher continues to fetch unaffected records if callCount == 1 { return result.Set{ "CVE-2023-1234": []result.Result{ { ID: "CVE-2023-1234", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, Constraint: createConstraint(t, "< 1:20.9.0-1.module_el8.9.0+1234+abcd", version.RpmFormat), PackageQualifiers: []qualifier.Qualifier{rpmmodularity.New("nodejs:20")}, }, }, Package: &testPkg, }, }, }, nil } // All other calls: return empty return result.Set{}, nil } _, err := almaLinuxMatchesWithUpstreams(mockProvider, testPkg) require.NoError(t, err) require.GreaterOrEqual(t, len(capturedCriteria), 2, "FindResults should be called for both RHEL disclosures and AlmaLinux advisories") // Helper to check if a criteria set includes OnlyQualifiedPackages hasQualifierCriterion := func(criteriaSet []vulnerability.Criteria) bool { for _, criterion := range criteriaSet { // Test if this criterion filters by qualifiers matches, _, err := criterion.MatchesVulnerability(vulnerability.Vulnerability{ PackageQualifiers: []qualifier.Qualifier{rpmmodularity.New("nodejs:22")}, }) require.NoError(t, err) if !matches { // This criterion rejected nodejs:22 when package has nodejs:20 return true } } return false } // Call 1: RHEL disclosures for binary package assert.True(t, hasQualifierCriterion(capturedCriteria[0]), "RHEL disclosure fetch should include OnlyQualifiedPackages criterion") // Call 2: AlmaLinux unaffected records assert.True(t, hasQualifierCriterion(capturedCriteria[1]), "AlmaLinux advisory fetch should include OnlyQualifiedPackages criterion") } // Comprehensive table-driven test for AlmaLinux matching func TestAlmaLinuxMatching(t *testing.T) { tests := []struct { name string description string // Input data pkg pkg.Package rhelVulns []vulnerability.Vulnerability almaVulns []vulnerability.Vulnerability // Expected behavior expectedMatches []match.Match }{ { name: "simple vulnerability match: disclosure without unaffected record", description: "Package version satisfies RHEL vulnerability constraint, no AlmaLinux unaffected record", pkg: pkg.Package{ Name: "httpd", Version: "2.4.37-10.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8", }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2023-1234", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateNotFixed, }, }, }, // No AlmaLinux unaffected records almaVulns: []vulnerability.Vulnerability{}, expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2023-1234", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateNotFixed, }, }, Package: pkg.Package{ Name: "httpd", Version: "2.4.37-10.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8", }, }, Details: createExpectedDetails(pkg.Package{ Name: "httpd", Version: "2.4.37-10.el8", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1234", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), }), }, }, }, { name: "simple vulnerability filtered by AlmaLinux unaffected record", description: "Package version satisfies AlmaLinux unaffected constraint, should be filtered out", pkg: pkg.Package{ Name: "httpd", Version: "2.4.37-51.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8", }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2023-1234", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 2.4.37-50.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-50.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "ALSA-2023:1234", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2023-1234"}, }, Constraint: createConstraint(t, ">= 2.4.37-51.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-51.el8"}, State: vulnerability.FixStateFixed, }, Unaffected: true, }, }, // Package version >= fix version, so vulnerability should be filtered expectedMatches: nil, }, { name: "fix replacement: simple non-modular package", description: "Non-modular package is vulnerable, RHEL fix info replaced by AlmaLinux fix info", pkg: pkg.Package{ Name: "httpd", Version: "2.4.37-10.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8", }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2021-44790", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 2.4.37-50.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-50.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "ALSA-2022:0123", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2021-44790"}, }, // Package 2.4.37-10.el8 < fix 2.4.37-43.el8, so it's still vulnerable Constraint: createConstraint(t, ">= 2.4.37-43.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-43.el8"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2022:0123", Link: "https://errata.almalinux.org/8/ALSA-2022-0123.html", }, }, Unaffected: true, }, }, // Package is vulnerable but fix info should be from AlmaLinux expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2021-44790", Namespace: "redhat:distro:redhat:8", }, // Constraint should be updated to match AlmaLinux fix version Constraint: createConstraint(t, "< 2.4.37-43.el8", version.RpmFormat), // Fix version should be replaced with AlmaLinux version Fix: vulnerability.Fix{ Versions: []string{"2.4.37-43.el8"}, State: vulnerability.FixStateFixed, }, // Advisory should be from AlmaLinux Advisories: []vulnerability.Advisory{ { ID: "ALSA-2022:0123", Link: "https://errata.almalinux.org/8/ALSA-2022-0123.html", }, }, }, Package: pkg.Package{ Name: "httpd", Version: "2.4.37-10.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8", }, }, Details: createExpectedDetails(pkg.Package{ Name: "httpd", Version: "2.4.37-10.el8", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-44790", Namespace: "redhat:distro:redhat:8", }, // Details should reflect the AlmaLinux constraint Constraint: createConstraint(t, "< 2.4.37-43.el8", version.RpmFormat), }), }, }, }, { name: "fix replacement: modular package with qualifiers", description: "Modular package with modularity qualifiers - RHEL fix replaced by AlmaLinux fix, ALSA maps to multiple CVEs", pkg: pkg.Package{ ID: pkg.ID("httpd-scenario1"), Name: "httpd", Version: "2.4.37-50.module_el8.7.0+3405+9516b832", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.7", }, Metadata: pkg.RpmMetadata{ ModularityLabel: strPtr("httpd:2.4:8070020220920142155:f8e95b4e"), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2006-20001", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 0:2.4.37-51.module+el8.7.0+18026+7b169787.1", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"0:2.4.37-51.module+el8.7.0+18026+7b169787.1"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "RHSA-2023:0852", Link: "https://access.redhat.com/errata/RHSA-2023:0852", }, }, PackageQualifiers: []qualifier.Qualifier{rpmmodularity.New("httpd:2.4")}, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "ALSA-2023:0852", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2006-20001"}, {ID: "CVE-2022-36760"}, {ID: "CVE-2022-37436"}, }, Constraint: createConstraint(t, ">= 2.4.37-51.module_el8.7.0+3405+9516b832.1", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-51.module_el8.7.0+3405+9516b832.1"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2023:0852", Link: "https://errata.almalinux.org/8/ALSA-2023-0852.html", }, }, PackageQualifiers: []qualifier.Qualifier{rpmmodularity.New("httpd:2.4")}, Unaffected: true, }, }, // Package version 2.4.37-50 < fix 2.4.37-51, vulnerable but with AlmaLinux fix info expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2006-20001", Namespace: "redhat:distro:redhat:8", }, // Constraint should be updated to match AlmaLinux fix version Constraint: createConstraint(t, "< 2.4.37-51.module_el8.7.0+3405+9516b832.1", version.RpmFormat), // Fix version should be from AlmaLinux (not RHEL) Fix: vulnerability.Fix{ Versions: []string{"2.4.37-51.module_el8.7.0+3405+9516b832.1"}, State: vulnerability.FixStateFixed, }, // Advisory should be from AlmaLinux (not RHEL) Advisories: []vulnerability.Advisory{ { ID: "ALSA-2023:0852", Link: "https://errata.almalinux.org/8/ALSA-2023-0852.html", }, }, // PackageQualifiers ignored in comparison }, Package: pkg.Package{ ID: pkg.ID("httpd-scenario1"), Name: "httpd", Version: "2.4.37-50.module_el8.7.0+3405+9516b832", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.7", }, Metadata: pkg.RpmMetadata{ ModularityLabel: strPtr("httpd:2.4:8070020220920142155:f8e95b4e"), }, }, Details: createExpectedDetails(pkg.Package{ Name: "httpd", Version: "2.4.37-50.module_el8.7.0+3405+9516b832", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8.7"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2006-20001", Namespace: "redhat:distro:redhat:8", }, // Details should reflect the AlmaLinux constraint Constraint: createConstraint(t, "< 2.4.37-51.module_el8.7.0+3405+9516b832.1", version.RpmFormat), }), }, }, }, { name: "Scenario 2A: A-advisory filters vulnerability when package at fix version", description: "RHEL has no fix, AlmaLinux A-advisory has fix, package version >= fix", pkg: pkg.Package{ ID: pkg.ID("open-vm-tools-scenario2a"), Name: "open-vm-tools", Version: "12.3.5-2.el8.alma.1", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.0", }, Metadata: pkg.RpmMetadata{ ModularityLabel: nil, }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "open-vm-tools", Reference: vulnerability.Reference{ ID: "CVE-2025-22247", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{}, State: vulnerability.FixStateNotFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "open-vm-tools", Reference: vulnerability.Reference{ ID: "ALSA-2025:A001", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2025-22247"}, }, Constraint: createConstraint(t, ">= 12.3.5-2.el8.alma.1", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"12.3.5-2.el8.alma.1"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2025:A001", Link: "https://errata.almalinux.org/8/ALSA-2025-A001.html", }, }, Unaffected: true, }, }, // Package version >= fix, should be filtered out expectedMatches: nil, }, { name: "Scenario 2B: A-advisory reports vulnerability with fix when package below fix version", description: "RHEL has no fix, AlmaLinux A-advisory has fix, package version < fix", pkg: pkg.Package{ ID: pkg.ID("open-vm-tools-scenario2b"), Name: "open-vm-tools", Version: "12.3.5-1.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.0", }, Metadata: pkg.RpmMetadata{ ModularityLabel: nil, }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "open-vm-tools", Reference: vulnerability.Reference{ ID: "CVE-2025-22247", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{}, State: vulnerability.FixStateNotFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "open-vm-tools", Reference: vulnerability.Reference{ ID: "ALSA-2025:A001", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2025-22247"}, }, Constraint: createConstraint(t, ">= 12.3.5-2.el8.alma.1", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"12.3.5-2.el8.alma.1"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2025:A001", Link: "https://errata.almalinux.org/8/ALSA-2025-A001.html", }, }, Unaffected: true, }, }, // Package version < fix, should report with AlmaLinux fix info expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "open-vm-tools", Reference: vulnerability.Reference{ ID: "CVE-2025-22247", Namespace: "redhat:distro:redhat:8", }, // Constraint should be updated to match AlmaLinux fix version Constraint: createConstraint(t, "< 12.3.5-2.el8.alma.1", version.RpmFormat), // Fix should be from AlmaLinux A-advisory (RHEL has no fix) Fix: vulnerability.Fix{ Versions: []string{"12.3.5-2.el8.alma.1"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2025:A001", Link: "https://errata.almalinux.org/8/ALSA-2025-A001.html", }, }, }, Package: pkg.Package{ ID: pkg.ID("open-vm-tools-scenario2b"), Name: "open-vm-tools", Version: "12.3.5-1.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.0", }, Metadata: pkg.RpmMetadata{ ModularityLabel: nil, }, }, Details: createExpectedDetails(pkg.Package{ Name: "open-vm-tools", Version: "12.3.5-1.el8", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8.0"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2025-22247", Namespace: "redhat:distro:redhat:8", }, // Details should reflect the AlmaLinux constraint Constraint: createConstraint(t, "< 12.3.5-2.el8.alma.1", version.RpmFormat), }), }, }, }, { name: "Scenario 3A: Module build number mismatch - AlmaLinux lower build filters vulnerability", description: "python38 with modularity - AlmaLinux build 3633 vs RHEL build 19642, package at AlmaLinux fix", pkg: pkg.Package{ ID: pkg.ID("python38-scenario3a"), Name: "python38", Version: "3.8.17-2.module_el8.9.0+3633+e453b53a", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.9", }, Metadata: pkg.RpmMetadata{ ModularityLabel: strPtr("python38:3.8:8090020230810123456:3b72e4d2"), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "python38", Reference: vulnerability.Reference{ ID: "CVE-2007-4559", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 0:3.8.17-2.module+el8.9.0+19642+a12b4af6", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"0:3.8.17-2.module+el8.9.0+19642+a12b4af6"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "RHSA-2023:7050", Link: "https://access.redhat.com/errata/RHSA-2023:7050", }, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "python38", Reference: vulnerability.Reference{ ID: "ALSA-2023:7050", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2007-4559"}, {ID: "CVE-2023-32681"}, }, Constraint: createConstraint(t, ">= 3.8.17-2.module_el8.9.0+3633+e453b53a", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"3.8.17-2.module_el8.9.0+3633+e453b53a"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2023:7050", Link: "https://errata.almalinux.org/8/ALSA-2023-7050.html", }, }, Unaffected: true, }, }, // Package has AlmaLinux build 3633, despite being < RHEL's 19642, should be filtered expectedMatches: nil, }, { name: "Scenario 3B: Module build number mismatch - vulnerable version still reported", description: "python38 with modularity - package version below AlmaLinux fix (despite lower build number)", pkg: pkg.Package{ ID: pkg.ID("python38-scenario3b"), Name: "python38", Version: "3.8.17-1.module_el8.9.0+3633+e453b53a", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.9", }, Metadata: pkg.RpmMetadata{ ModularityLabel: strPtr("python38:3.8:8090020230810123456:3b72e4d2"), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "python38", Reference: vulnerability.Reference{ ID: "CVE-2007-4559", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 0:3.8.17-2.module+el8.9.0+19642+a12b4af6", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"0:3.8.17-2.module+el8.9.0+19642+a12b4af6"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "RHSA-2023:7050", Link: "https://access.redhat.com/errata/RHSA-2023:7050", }, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "python38", Reference: vulnerability.Reference{ ID: "ALSA-2023:7050", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2007-4559"}, {ID: "CVE-2023-32681"}, }, Constraint: createConstraint(t, ">= 3.8.17-2.module_el8.9.0+3633+e453b53a", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"3.8.17-2.module_el8.9.0+3633+e453b53a"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2023:7050", Link: "https://errata.almalinux.org/8/ALSA-2023-7050.html", }, }, Unaffected: true, }, }, // Package version 3.8.17-1 < fix 3.8.17-2, should report with AlmaLinux fix expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "python38", Reference: vulnerability.Reference{ ID: "CVE-2007-4559", Namespace: "redhat:distro:redhat:8", }, // Constraint should be updated to match AlmaLinux fix version Constraint: createConstraint(t, "< 3.8.17-2.module_el8.9.0+3633+e453b53a", version.RpmFormat), // Fix should be from AlmaLinux (lower build number 3633, not RHEL's 19642) Fix: vulnerability.Fix{ Versions: []string{"3.8.17-2.module_el8.9.0+3633+e453b53a"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2023:7050", Link: "https://errata.almalinux.org/8/ALSA-2023-7050.html", }, }, }, Package: pkg.Package{ ID: pkg.ID("python38-scenario3b"), Name: "python38", Version: "3.8.17-1.module_el8.9.0+3633+e453b53a", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.9", }, Metadata: pkg.RpmMetadata{ ModularityLabel: strPtr("python38:3.8:8090020230810123456:3b72e4d2"), }, }, Details: createExpectedDetails(pkg.Package{ Name: "python38", Version: "3.8.17-1.module_el8.9.0+3633+e453b53a", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8.9"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2007-4559", Namespace: "redhat:distro:redhat:8", }, // Details should reflect the AlmaLinux constraint Constraint: createConstraint(t, "< 3.8.17-2.module_el8.9.0+3633+e453b53a", version.RpmFormat), }), }, }, }, { name: "Scenario 4: Wont-fix vulnerability reported when no AlmaLinux unaffected record", description: "tar package - RHEL wont-fix, no AlmaLinux unaffected record, vulnerability reported", pkg: pkg.Package{ ID: pkg.ID("tar-scenario4"), Name: "tar", Version: "2:1.30-5.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.0", }, Metadata: pkg.RpmMetadata{ ModularityLabel: nil, }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "tar", Reference: vulnerability.Reference{ ID: "CVE-2005-2541", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{}, State: vulnerability.FixStateWontFix, }, }, }, // NO AlmaLinux unaffected record - AlmaLinux follows RHEL's wont-fix almaVulns: []vulnerability.Vulnerability{}, // Vulnerability should be reported with RHEL wont-fix state expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "tar", Reference: vulnerability.Reference{ ID: "CVE-2005-2541", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{}, State: vulnerability.FixStateWontFix, }, }, Package: pkg.Package{ ID: pkg.ID("tar-scenario4"), Name: "tar", Version: "2:1.30-5.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.0", }, Metadata: pkg.RpmMetadata{ ModularityLabel: nil, }, }, Details: createExpectedDetails(pkg.Package{ Name: "tar", Version: "2:1.30-5.el8", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8.0"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2005-2541", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, ">= 0", version.RpmFormat), }), }, }, }, { name: "Upstream match: binary package vulnerable via source package with fix replacement", description: "Binary package python3-tkinter with upstream python3 - RHEL disclosure for source, AlmaLinux fixes binary", pkg: pkg.Package{ ID: pkg.ID("python3-tkinter-upstream"), Name: "python3-tkinter", Version: "3.6.8-40.el8_6", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "python3", Version: "3.6.8-40.el8_6", }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "python3", Reference: vulnerability.Reference{ ID: "CVE-2007-4559", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 0:3.6.8-56.el8_9", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"0:3.6.8-56.el8_9"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "python3-tkinter", Reference: vulnerability.Reference{ ID: "ALSA-2023:7151", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2007-4559"}, }, Constraint: createConstraint(t, ">= 3.6.8-56.el8_9.alma.1", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"3.6.8-56.el8_9.alma.1"}, State: vulnerability.FixStateFixed, }, Unaffected: true, }, }, // Package version 3.6.8-40 < fix 3.6.8-56, vulnerable with AlmaLinux fix info expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "python3", Reference: vulnerability.Reference{ ID: "CVE-2007-4559", Namespace: "redhat:distro:redhat:8", }, // Constraint should be updated to match AlmaLinux fix version Constraint: createConstraint(t, "< 3.6.8-56.el8_9.alma.1", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"3.6.8-56.el8_9.alma.1"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2023:7151", Link: "https://errata.almalinux.org/8/ALSA-2023-7151.html", }, }, }, Package: pkg.Package{ ID: pkg.ID("python3-tkinter-upstream"), Name: "python3-tkinter", Version: "3.6.8-40.el8_6", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "python3", Version: "3.6.8-40.el8_6", }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, // Details should show ExactIndirectMatch since vulnerability is via upstream Details: []match.Detail{{ Type: match.ExactIndirectMatch, Matcher: match.RpmMatcher, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "almalinux", Version: "8.10", }, Package: match.PackageParameter{ Name: "python3-tkinter", Version: "3.6.8-40.el8_6", }, Namespace: "redhat:distro:redhat:8", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2007-4559", VersionConstraint: "< 3.6.8-56.el8_9.alma.1 (rpm)", }, Confidence: 1.0, }}, }, }, }, { name: "Alias handling: RHEL CVEs filtered by AlmaLinux ALSA with related vulnerabilities", description: "RHEL has 2 CVEs, AlmaLinux ALSA relates to one, package >= fix filters that CVE", pkg: pkg.Package{ Name: "httpd", Version: "2.4.37-47.el8.alma", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.7", }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2023-1234", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 2.4.37-50.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-50.el8"}, State: vulnerability.FixStateFixed, }, }, { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2023-5678", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 2.4.37-50.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-50.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "httpd", Reference: vulnerability.Reference{ ID: "ALSA-2023:1234", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2023-1234"}, // ALSA aliases CVE }, // Package version 47 >= fix version 40, so CVE-2023-1234 is filtered Constraint: createConstraint(t, ">= 2.4.37-40.el8.alma", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-40.el8.alma"}, State: vulnerability.FixStateFixed, }, Unaffected: true, }, }, // CVE-2023-1234 filtered by AlmaLinux unaffected record // Only CVE-2023-5678 should remain (no AlmaLinux unaffected record for it) expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "httpd", Reference: vulnerability.Reference{ ID: "CVE-2023-5678", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 2.4.37-50.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"2.4.37-50.el8"}, State: vulnerability.FixStateFixed, }, }, Package: pkg.Package{ Name: "httpd", Version: "2.4.37-47.el8.alma", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.7", }, }, Details: createExpectedDetails(pkg.Package{ Name: "httpd", Version: "2.4.37-47.el8.alma", Distro: &distro.Distro{Type: distro.AlmaLinux, Version: "8.7"}, }, vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-5678", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 2.4.37-50.el8", version.RpmFormat), }), }, }, }, { name: "Epoch mismatch regression: no false positive from binary/source epoch difference", description: "Binary package epoch 0, upstream source package version without epoch, RHEL constraint has epoch 4", pkg: pkg.Package{ ID: pkg.ID("perl-errno-epoch"), Name: "perl-Errno", Version: "0:1.28-422.el8.0.1", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-422.el8.0.1", // No epoch in sourceRPM metadata }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "perl", Reference: vulnerability.Reference{ ID: "CVE-2020-10543", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 4:5.26.3-419.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"4:5.26.3-419.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{}, // Package version 5.26.3-422 > fix 5.26.3-419, NOT vulnerable // This is a regression test: the bug was comparing binary epoch (0) against constraint with source epoch (4), // causing false positive: 0:1.28-422.el8.0.1 < 4:5.26.3-419.el8 = true (WRONG!) // Correct behavior: Don't add binary epoch to source version, compare without epochs expectedMatches: nil, }, { name: "Epoch handling: upstream vulnerable, no AlmaLinux advisory", description: "Binary package with upstream source, source version < RHEL fix, no AlmaLinux advisory → match", pkg: pkg.Package{ ID: pkg.ID("perl-errno-vulnerable"), Name: "perl-Errno", Version: "0:1.28-416.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-416.el8", // No epoch, version 416 < fix 419 }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "perl", Reference: vulnerability.Reference{ ID: "CVE-2020-10543", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 4:5.26.3-419.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"4:5.26.3-419.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{}, // Package version 5.26.3-416 < fix 5.26.3-419, IS vulnerable // No AlmaLinux advisory, so RHEL disclosure should be reported expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "perl", Reference: vulnerability.Reference{ ID: "CVE-2020-10543", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 4:5.26.3-419.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"4:5.26.3-419.el8"}, State: vulnerability.FixStateFixed, }, }, Package: pkg.Package{ ID: pkg.ID("perl-errno-vulnerable"), Name: "perl-Errno", Version: "0:1.28-416.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-416.el8", }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, // Details should show ExactIndirectMatch since vulnerability is via upstream Details: []match.Detail{{ Type: match.ExactIndirectMatch, Matcher: match.RpmMatcher, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "almalinux", Version: "8.10", }, Package: match.PackageParameter{ Name: "perl-Errno", Version: "0:1.28-416.el8", }, Namespace: "redhat:distro:redhat:8", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-10543", VersionConstraint: "< 4:5.26.3-419.el8 (rpm)", }, Confidence: 1.0, }}, }, }, }, { name: "Epoch handling: upstream vulnerable, AlmaLinux fixes binary package", description: "Binary package with upstream source, source version < RHEL fix, AlmaLinux fixes binary → filtered", pkg: pkg.Package{ ID: pkg.ID("perl-errno-alma-fixed"), Name: "perl-Errno", Version: "0:1.28-416.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-416.el8", // No epoch, version 416 < RHEL fix 419 }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "perl", Reference: vulnerability.Reference{ ID: "CVE-2020-10543", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 4:5.26.3-419.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"4:5.26.3-419.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "perl-Errno", Reference: vulnerability.Reference{ ID: "ALSA-2020:4514", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2020-10543"}, }, // AlmaLinux fixes at binary package version 1.28-416.el8, marking it unaffected Constraint: createConstraint(t, ">= 1.28-416.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"1.28-416.el8"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2020:4514", Link: "https://errata.almalinux.org/8/ALSA-2020-4514.html", }, }, Unaffected: true, }, }, // Even though upstream source is vulnerable per RHEL, // AlmaLinux marks this binary package version as unaffected // Package version 1.28-416.el8 >= AlmaLinux fix 1.28-416.el8 → filtered expectedMatches: nil, }, { name: "Epoch handling: upstream vulnerable, AlmaLinux fix on binary, package below fix", description: "Binary package with upstream source, source version < RHEL fix, AlmaLinux has fix but package < fix → match with AlmaLinux fix", pkg: pkg.Package{ ID: pkg.ID("perl-errno-needs-alma-fix"), Name: "perl-Errno", Version: "0:1.28-415.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-415.el8", // No epoch, version 415 < RHEL fix 419 }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, rhelVulns: []vulnerability.Vulnerability{ { PackageName: "perl", Reference: vulnerability.Reference{ ID: "CVE-2020-10543", Namespace: "redhat:distro:redhat:8", }, Constraint: createConstraint(t, "< 4:5.26.3-419.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"4:5.26.3-419.el8"}, State: vulnerability.FixStateFixed, }, }, }, almaVulns: []vulnerability.Vulnerability{ { PackageName: "perl-Errno", Reference: vulnerability.Reference{ ID: "ALSA-2020:4514", Namespace: "almalinux:distro:almalinux:8", }, RelatedVulnerabilities: []vulnerability.Reference{ {ID: "CVE-2020-10543"}, }, // AlmaLinux fixes at binary package version 1.28-418.el8 Constraint: createConstraint(t, ">= 1.28-418.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"1.28-418.el8"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2020:4514", Link: "https://errata.almalinux.org/8/ALSA-2020-4514.html", }, }, Unaffected: true, }, }, // Upstream source is vulnerable per RHEL (415 < 419) // Binary package version 1.28-415.el8 < AlmaLinux fix 1.28-418.el8 // Should report vulnerability with AlmaLinux fix info (fix replacement) expectedMatches: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ PackageName: "perl", Reference: vulnerability.Reference{ ID: "CVE-2020-10543", Namespace: "redhat:distro:redhat:8", }, // Constraint should be updated to match AlmaLinux fix version Constraint: createConstraint(t, "< 1.28-418.el8", version.RpmFormat), Fix: vulnerability.Fix{ Versions: []string{"1.28-418.el8"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{ { ID: "ALSA-2020:4514", Link: "https://errata.almalinux.org/8/ALSA-2020-4514.html", }, }, }, Package: pkg.Package{ ID: pkg.ID("perl-errno-needs-alma-fix"), Name: "perl-Errno", Version: "0:1.28-415.el8", Type: syftPkg.RpmPkg, Distro: &distro.Distro{ Type: distro.AlmaLinux, Version: "8.10", }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-415.el8", }, }, Metadata: pkg.RpmMetadata{ Epoch: intPtr(0), }, }, // Details should show ExactIndirectMatch since vulnerability is via upstream Details: []match.Detail{{ Type: match.ExactIndirectMatch, Matcher: match.RpmMatcher, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "almalinux", Version: "8.10", }, Package: match.PackageParameter{ Name: "perl-Errno", Version: "0:1.28-415.el8", }, Namespace: "redhat:distro:redhat:8", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-10543", VersionConstraint: "< 1.28-418.el8 (rpm)", }, Confidence: 1.0, }}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock provider with all vulnerabilities allVulns := append(tt.rhelVulns, tt.almaVulns...) mockProvider := &MockProvider{ findResultsFunc: func(criteria ...vulnerability.Criteria) (result.Set, error) { // Use the mock vulnerability provider to filter vulnProvider := mock.VulnerabilityProvider(allVulns...) vulns, err := vulnProvider.FindVulnerabilities(criteria...) if err != nil { return nil, err } // Convert to result.Set with Details fully populated resultSet := make(result.Set) for _, vuln := range vulns { r := result.Result{ ID: vuln.ID, Vulnerabilities: []vulnerability.Vulnerability{vuln}, Package: &tt.pkg, // Details must be fully populated per the matcher contract Details: []match.Detail{{ Type: match.ExactDirectMatch, Matcher: match.RpmMatcher, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: tt.pkg.Distro.Type.String(), Version: tt.pkg.Distro.Version, }, Package: match.PackageParameter{ Name: tt.pkg.Name, Version: tt.pkg.Version, }, Namespace: vuln.Namespace, }, Found: match.DistroResult{ VulnerabilityID: vuln.ID, VersionConstraint: vuln.Constraint.String(), }, Confidence: 1.0, }}, } resultSet[vuln.ID] = append(resultSet[vuln.ID], r) } return resultSet, nil }, } // Call the matcher matches, err := almaLinuxMatchesWithUpstreams(mockProvider, tt.pkg) require.NoError(t, err) // Compare matches using cmp.Diff // Only ignore: // - PackageQualifiers (tested separately, have unexported fields in implementations) // - Unexported fields within structs if diff := cmp.Diff(tt.expectedMatches, matches, cmpopts.IgnoreUnexported(match.Match{}, match.Detail{}, pkg.Package{}, pkg.RpmMetadata{}, file.LocationSet{}, distro.Distro{}), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "PackageQualifiers")); diff != "" { t.Errorf("matches mismatch (-want +got):\n%s", diff) } }) } } // Helper functions for tests func createConstraint(t *testing.T, constraintStr string, format version.Format) version.Constraint { constraint, err := version.GetConstraint(constraintStr, format) require.NoError(t, err) return constraint } func createExpectedDetails(pkg pkg.Package, vuln vulnerability.Vulnerability) []match.Detail { return []match.Detail{{ Type: match.ExactDirectMatch, Matcher: match.RpmMatcher, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: pkg.Distro.Type.String(), Version: pkg.Distro.Version, }, Package: match.PackageParameter{ Name: pkg.Name, Version: pkg.Version, }, Namespace: vuln.Reference.Namespace, }, Found: match.DistroResult{ VulnerabilityID: vuln.Reference.ID, VersionConstraint: vuln.Constraint.String(), }, Confidence: 1.0, }} } func strPtr(s string) *string { return &s } ================================================ FILE: grype/matcher/rpm/matcher.go ================================================ package rpm import ( "errors" "fmt" "strings" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { MissingEpochStrategy version.MissingEpochStrategy UseCPEsForEOL bool } func NewRpmMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.RpmPkg} } func (m *Matcher) Type() match.MatcherType { return match.RpmMatcher } //nolint:funlen func (m *Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { var matches []match.Match // Handle AlmaLinux matching at the top level before the binary/upstream split // AlmaLinux matching needs to handle both binary and upstream packages internally if p.Distro != nil && shouldUseAlmaLinuxMatching(p.Distro) { almaMatches, err := m.matchAlmaLinux(vp, p) if err != nil { return nil, nil, fmt.Errorf("failed to match AlmaLinux: %w", err) } matches = append(matches, almaMatches...) } else { // For non-AlmaLinux distros, use the standard binary/upstream split exactMatches, err := m.matchPackage(vp, p) if err != nil { return nil, nil, fmt.Errorf("failed to match by exact package name: %w", err) } matches = append(matches, exactMatches...) sourceMatches, err := m.matchUpstreamPackages(vp, p) if err != nil { return nil, nil, fmt.Errorf("failed to match by source indirection: %w", err) } matches = append(matches, sourceMatches...) } // if configured, also search by CPEs for packages from EOL distros if m.cfg.UseCPEsForEOL && internal.IsDistroEOL(vp, p.Distro) { log.WithFields("package", p.Name, "distro", p.Distro).Debug("distro is EOL, searching by CPEs") cpeMatches, err := internal.MatchPackageByCPEs(vp, p, m.Type()) switch { case errors.Is(err, internal.ErrEmptyCPEMatch): log.WithFields("package", p.Name).Debug("package has no CPEs for EOL fallback matching") case err != nil: log.WithFields("package", p.Name, "error", err).Debug("failed to match by CPEs for EOL distro") default: matches = append(matches, cpeMatches...) } } return matches, nil, nil } // matchAlmaLinux handles AlmaLinux-specific matching logic that considers both binary and upstream packages // This must be called at the top level (before the binary/upstream split) because AlmaLinux matching // needs to search for RHEL disclosures for both the binary package and its upstreams, then filter // using AlmaLinux unaffected records for both the binary package and related packages func (m *Matcher) matchAlmaLinux(vp vulnerability.Provider, p pkg.Package) ([]match.Match, error) { if p.Distro == nil { return nil, nil } if isUnknownVersion(p.Version) { log.WithFields("package", p.Name).Trace("skipping package with unknown version") return nil, nil } provider := result.NewProvider(vp, p, m.Type()) // Add epoch if applicable for the binary package binaryPkg := p addEpochIfApplicable(&binaryPkg) // Call almaLinuxMatches with both the binary package and its upstreams return almaLinuxMatchesWithUpstreams(provider, binaryPkg) } // matchPackage matches the given package against the vulnerability provider (direct match). // // Regarding RPM epochs... we know that the package and vulnerability will have // well-specified epochs since both are sourced from either the RPM DB directly or // the upstream RedHat vulnerability data. Note: this is very much UNLIKE our // matching on a source package above where the epoch could be dropped in the // reference data. This means that any missing epoch CAN be assumed to be zero, // as it falls into the case of "the project elected to NOT have an epoch for the // first version scheme" and not into any other case. // // For this reason match exactly on a package, we should be EXPLICIT about the // epoch (since downstream version comparison logic will strip the epoch during // comparison for the above-mentioned reasons --essentially for the source RPM // case). To do this, we fill in missing epoch values in the package versions with // an explicit 0. func (m *Matcher) matchPackage(vp vulnerability.Provider, p pkg.Package) ([]match.Match, error) { provider := result.NewProvider(vp, p, m.Type()) // we want to ensure that the version ALWAYS has an epoch specified... but at the same time we do not want to modify the // original package that was passed in when making matches. This is why we create the provider with the original package // then patch the epoch into the version of the package that we are searching with. addEpochIfApplicable(&p) matches, err := m.findMatches(provider, p) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities by dpkg source indirection: %w", err) } return matches, nil } // matchUpstreamPackages finds matches with a synthetic package based on the sourceRPM (indirect match). // Regarding RPM epoch and comparisons... RedHat is explicit that when an RPM // epoch is not specified that it should be assumed to be zero (see // https://github.com/rpm-software-management/rpm/issues/450). This comment from // RedHat is applicable for a project that has elected to not use epoch and has // not changed their version scheme at all --therefore it is safe to assume that // the epoch (though not specified) is 0. However, in cases where there may be a // non-zero epoch and it has been omitted from the version string, it is NOT safe // to assume an epoch of 0... as this could lead to misleading comparison // results. // For example, take the perl-Errno package: // name: perl-Errno // version: 0:1.28-419.el8_4.1 // sourceRPM: perl-5.26.3-419.el8_4.1.src.rpm // Say we have a vulnerability with the following information (note this is // against the SOURCE package "perl", not the target package, "perl-Errno"): // ID: CVE-2020-10543 // Package Name: perl // Version constraint: < 4:5.26.3-419.el8 // Note that the vulnerability information has complete knowledge about the // version and it's lineage (epoch + version), however, the source package // information for perl-Errno does not include any information about epoch. With // the rule from RedHat we should assume a 0 epoch and make the comparison: // 0:5.26.3-419.el8 < 4:5.26.3-419.el8 = true! ... therefore, we've been vulnerable since epoch 0 < 4. // ... this is an INVALID comparison! // The problem with this is that sourceRPMs tend to not specify epoch even though // there may be a non-zero epoch for that package! This is important. The "more // correct" thing to do in this case is to drop the epoch: // 5.26.3-419.el8 < 5.26.3-419.el8 = false! ... these are the SAME VERSION // There is still a problem with this approach: it essentially makes an // assumption that a missing epoch really is the SAME epoch to the other version // being compared (in our example, no perl epoch on one side means we should // really assume an epoch of 4 on the other side). This could still lead to // problems since an epoch delimits potentially non-comparable version lineages. func (m *Matcher) matchUpstreamPackages(vp vulnerability.Provider, p pkg.Package) ([]match.Match, error) { provider := result.NewProvider(vp, p, m.Type()) var matches []match.Match for _, indirectPackage := range pkg.UpstreamPackages(p) { indirectMatches, err := m.findMatches(provider, indirectPackage) if err != nil { return nil, fmt.Errorf("failed to find vulnerabilities for rpm upstream source package: %w", err) } matches = append(matches, indirectMatches...) } return matches, nil } func (m *Matcher) findMatches(provider result.Provider, searchPkg pkg.Package) ([]match.Match, error) { if searchPkg.Distro == nil { return nil, nil } if isUnknownVersion(searchPkg.Version) { log.WithFields("package", searchPkg.Name).Trace("skipping package with unknown version") return nil, nil } switch { case shouldUseRedhatEUSMatching(searchPkg.Distro): return redhatEUSMatches(provider, searchPkg, m.cfg.MissingEpochStrategy) default: return m.standardMatches(provider, searchPkg) } } func (m *Matcher) standardMatches(provider result.Provider, searchPkg pkg.Package) ([]match.Match, error) { // Create version with config embedded pkgVersion := version.NewWithConfig( searchPkg.Version, pkg.VersionFormat(searchPkg), version.ComparisonConfig{ MissingEpochStrategy: m.cfg.MissingEpochStrategy, }, ) disclosures, err := provider.FindResults( search.ByPackageName(searchPkg.Name), search.ByDistro(*searchPkg.Distro), internal.OnlyQualifiedPackages(searchPkg), internal.OnlyVulnerableVersions(pkgVersion), ) if err != nil { return nil, fmt.Errorf("matcher failed to fetch disclosures for distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err) } return disclosures.ToMatches(), nil } func addEpochIfApplicable(p *pkg.Package) { meta, ok := p.Metadata.(pkg.RpmMetadata) ver := p.Version if ver == "" { return // no version to work with, so we should not bother with an epoch } switch { case strings.Contains(ver, ":"): // we already have an epoch embedded in the version string return case ok && meta.Epoch != nil: // we have an explicit epoch in the metadata p.Version = fmt.Sprintf("%d:%s", *meta.Epoch, ver) default: // no epoch was found, so we will add one p.Version = "0:" + ver } } func isUnknownVersion(v string) bool { return v == "" || strings.ToLower(v) == "unknown" } ================================================ FILE: grype/matcher/rpm/matcher_mocks_test.go ================================================ package rpm import ( "time" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" syftCpe "github.com/anchore/syft/syft/cpe" ) func newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) vulnerability.Provider { if withEpoch { return mock.VulnerabilityProvider(vulnerabilitiesWithEpoch(packageName, indirectName)...) } else if withPackageQualifiers { return mock.VulnerabilityProvider(vulnerabilitiesWithPackageQualifiers(packageName)...) } return mock.VulnerabilityProvider(vulnerabilitiesDefaults(packageName, indirectName)...) } const namespace = "secdb:distro:centos:8" func vulnerabilitiesDefaults(packageName, indirectName string) []vulnerability.Vulnerability { return []vulnerability.Vulnerability{ // direct... { PackageName: packageName, Constraint: version.MustGetConstraint("<= 7.1.3-6", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-1", Namespace: namespace}, }, // indirect... // expected... { PackageName: indirectName, Constraint: version.MustGetConstraint("< 7.1.4-5", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2014-fake-2", Namespace: namespace}, }, { PackageName: indirectName, Constraint: version.MustGetConstraint("< 8.0.2-0", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-3", Namespace: namespace}, }, // unexpected... { PackageName: indirectName, Constraint: version.MustGetConstraint("< 7.0.4-1", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2013-fake-BAD", Namespace: namespace}, }, } } func vulnerabilitiesWithEpoch(packageName, indirectName string) []vulnerability.Vulnerability { return []vulnerability.Vulnerability{ // direct... { PackageName: packageName, Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-1", Namespace: namespace}, }, { PackageName: packageName, Constraint: version.MustGetConstraint("<= 0:2.28-419.el8.", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-2", Namespace: namespace}, }, // indirect... { PackageName: indirectName, Constraint: version.MustGetConstraint("< 5.28.3-420.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-3", Namespace: namespace}, }, // unexpected... { PackageName: indirectName, Constraint: version.MustGetConstraint("< 4:5.26.3-419.el8", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-4", Namespace: namespace}, }, } } func vulnerabilitiesWithPackageQualifiers(packageName string) []vulnerability.Vulnerability { return []vulnerability.Vulnerability{ // direct... { PackageName: packageName, Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-1", Namespace: namespace}, PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.New("containertools:3"), }, }, { PackageName: packageName, Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-2", Namespace: namespace}, PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.New(""), }, }, { PackageName: packageName, Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-3", Namespace: namespace}, }, { PackageName: packageName, Constraint: version.MustGetConstraint("<= 0:1.0-419.el8.", version.RpmFormat), Reference: vulnerability.Reference{ID: "CVE-2021-4", Namespace: namespace}, PackageQualifiers: []qualifier.Qualifier{ rpmmodularity.New("containertools:4"), }, }, } } // mockEOLProvider wraps mock.VulnerabilityProvider and adds EOLChecker support for testing type mockEOLProvider struct { vulnerability.Provider eolDate *time.Time } func (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) { return m.eolDate, nil, nil } func newMockEOLProvider(eolDate *time.Time) *mockEOLProvider { // include CPE vulnerability for testing CPE fallback return &mockEOLProvider{ Provider: mock.VulnerabilityProvider([]vulnerability.Vulnerability{ // distro-based vulnerability { PackageName: "openssl", Reference: vulnerability.Reference{ID: "CVE-2014-distro-1", Namespace: namespace}, Constraint: version.MustGetConstraint("< 1.0.2", version.RpmFormat), }, // CPE-based vulnerability { PackageName: "openssl", Reference: vulnerability.Reference{ID: "CVE-2014-cpe-1", Namespace: "nvd:cpe"}, Constraint: version.MustGetConstraint("< 1.0.2", version.UnknownFormat), CPEs: []syftCpe.CPE{ syftCpe.Must("cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*", ""), }, }, }...), eolDate: eolDate, } } ================================================ FILE: grype/matcher/rpm/matcher_test.go ================================================ package rpm import ( "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftCpe "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcherRpm(t *testing.T) { tests := []struct { name string p pkg.Package setup func() (vulnerability.Provider, *distro.Distro, Matcher) expectedMatches map[string]match.Type wantErr bool }{ { name: "Rpm Match matches by direct and by source indirection", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron-libs", Version: "7.1.3-6", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ { Name: "neutron", Version: "7.1.3-6.el8", }, }, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("neutron-libs", "neutron", false, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2014-fake-1": match.ExactDirectMatch, "CVE-2014-fake-2": match.ExactIndirectMatch, "CVE-2013-fake-3": match.ExactIndirectMatch, }, }, { name: "Rpm Match matches by direct and ignores the source rpm when the package names are the same", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "7.1.3-6", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ { Name: "neutron", Version: "7.1.3-6.el8", }, }, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("neutron", "neutron-devel", false, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2014-fake-1": match.ExactDirectMatch, }, }, { // Regression against https://github.com/anchore/grype/issues/376 name: "Rpm Match matches by direct and by source indirection when the SourceRpm version is desynced from package version", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron-libs", Version: "7.1.3-6", Type: syftPkg.RpmPkg, Upstreams: []pkg.UpstreamPackage{ { Name: "neutron", Version: "17.16.3-229.el8", }, }, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("neutron-libs", "neutron", false, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2014-fake-1": match.ExactDirectMatch, }, }, { // Epoch in pkg but not in src package version, epoch found in the vuln record // Regression: https://github.com/anchore/grype/issues/437 name: "Rpm Match should not occur due to source match even though source has no epoch", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "0:1.28-419.el8_4.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{ Epoch: intRef(0), }, Upstreams: []pkg.UpstreamPackage{ { Name: "perl", Version: "5.26.3-419.el8_4.1", }, }, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("perl-Errno", "perl", true, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2021-2": match.ExactDirectMatch, "CVE-2021-3": match.ExactIndirectMatch, }, }, { name: "package without epoch is assumed to be 0 - compared against vuln with NO epoch (direct match only)", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "1.28-419.el8_4.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("perl-Errno", "doesn't-matter", false, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2014-fake-1": match.ExactDirectMatch, }, }, { name: "package without epoch is assumed to be 0 - compared against vuln WITH epoch (direct match only)", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "1.28-419.el8_4.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("perl-Errno", "doesn't-matter", true, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2021-2": match.ExactDirectMatch, }, }, { name: "package WITH epoch - compared against vuln with NO epoch (direct match only)", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "2:1.28-419.el8_4.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("perl-Errno", "doesn't-matter", false, false) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2014-fake-1": match.ExactDirectMatch, }, }, { name: "package WITH epoch - compared against vuln WITH epoch (direct match only)", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "perl-Errno", Version: "2:1.28-419.el8_4.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{}, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("perl-Errno", "doesn't-matter", true, false) return store, d, matcher }, expectedMatches: map[string]match.Type{}, }, { name: "package with modularity label 1", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "maniac", Version: "0.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("containertools:3:1234:5678"), }, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("maniac", "doesn't-matter", false, true) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2021-1": match.ExactDirectMatch, "CVE-2021-3": match.ExactDirectMatch, }, }, { name: "package with modularity label 2", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "maniac", Version: "0.1", Type: syftPkg.RpmPkg, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("containertools:1:abc:123"), }, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("maniac", "doesn't-matter", false, true) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2021-3": match.ExactDirectMatch, }, }, { name: "package without modularity label", p: pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "maniac", Version: "0.1", Type: syftPkg.RpmPkg, }, setup: func() (vulnerability.Provider, *distro.Distro, Matcher) { matcher := Matcher{} d := distro.New(distro.CentOS, "8", "") store := newMockProvider("maniac", "doesn't-matter", false, true) return store, d, matcher }, expectedMatches: map[string]match.Type{ "CVE-2021-1": match.ExactDirectMatch, "CVE-2021-2": match.ExactDirectMatch, "CVE-2021-3": match.ExactDirectMatch, "CVE-2021-4": match.ExactDirectMatch, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { store, d, matcher := test.setup() if test.p.Distro == nil { test.p.Distro = d } actual, _, err := matcher.Match(store, test.p) if err != nil { t.Fatal("could not find match: ", err) } assert.Len(t, actual, len(test.expectedMatches), "unexpected matches count") for _, a := range actual { if val, ok := test.expectedMatches[a.Vulnerability.ID]; !ok { t.Errorf("return unknown match CVE: %s", a.Vulnerability.ID) continue } else { require.NotEmpty(t, a.Details) for _, de := range a.Details { assert.Equal(t, val, de.Type) } } assert.Equal(t, test.p.Name, a.Package.Name, "failed to capture original package name") for _, detail := range a.Details { assert.Equal(t, matcher.Type(), detail.Matcher, "failed to capture matcher type") } } if t.Failed() { t.Logf("discovered CVES: %+v", actual) } }) } } func Test_addEpochIfApplicable(t *testing.T) { tests := []struct { name string pkg pkg.Package expected string }{ { name: "assume 0 epoch", pkg: pkg.Package{ Version: "3.26.0-6.el8", }, expected: "0:3.26.0-6.el8", }, { name: "epoch already exists in version string", pkg: pkg.Package{ Version: "7:3.26.0-6.el8", }, expected: "7:3.26.0-6.el8", }, { name: "epoch only exists in metadata", pkg: pkg.Package{ Version: "3.26.0-6.el8", Metadata: pkg.RpmMetadata{ Epoch: intRef(7), }, }, expected: "7:3.26.0-6.el8", }, { name: "epoch does not exist in metadata", pkg: pkg.Package{ Version: "3.26.0-6.el8", Metadata: pkg.RpmMetadata{ Epoch: nil, // assume 0 epoch }, }, expected: "0:3.26.0-6.el8", }, { name: "version is empty", pkg: pkg.Package{ Version: "", Metadata: pkg.RpmMetadata{ Epoch: nil, // assume 0 epoch }, }, expected: "", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { p := test.pkg addEpochIfApplicable(&p) assert.Equal(t, test.expected, p.Version) }) } } func TestMatcherRpm_CPEFallbackWhenEOL(t *testing.T) { pastEOL := time.Now().AddDate(-1, 0, 0) // 1 year ago futureEOL := time.Now().AddDate(1, 0, 0) // 1 year from now d := distro.New(distro.CentOS, "8", "") // package with CPEs for CPE-based matching p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "openssl", Version: "1.0.1", Type: syftPkg.RpmPkg, Distro: d, CPEs: []syftCpe.CPE{ syftCpe.Must("cpe:2.3:a:openssl:openssl:1.0.1:*:*:*:*:*:*:*", ""), }, } tests := []struct { name string useCPEsForEOL bool eolDate *time.Time expectCPEMatches bool }{ { name: "CPE fallback enabled and distro is EOL - should include CPE matches", useCPEsForEOL: true, eolDate: &pastEOL, expectCPEMatches: true, }, { name: "CPE fallback enabled but distro not EOL - should not include CPE matches", useCPEsForEOL: true, eolDate: &futureEOL, expectCPEMatches: false, }, { name: "CPE fallback disabled and distro is EOL - should not include CPE matches", useCPEsForEOL: false, eolDate: &pastEOL, expectCPEMatches: false, }, { name: "CPE fallback disabled and distro not EOL - should not include CPE matches", useCPEsForEOL: false, eolDate: &futureEOL, expectCPEMatches: false, }, { name: "CPE fallback enabled but no EOL data - should not include CPE matches", useCPEsForEOL: true, eolDate: nil, expectCPEMatches: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { matcher := NewRpmMatcher(MatcherConfig{ UseCPEsForEOL: tt.useCPEsForEOL, }) vp := newMockEOLProvider(tt.eolDate) matches, _, err := matcher.Match(vp, p) require.NoError(t, err) // check if any CPE matches were found hasCPEMatch := false for _, m := range matches { for _, detail := range m.Details { if detail.Type == match.CPEMatch { hasCPEMatch = true break } } } if tt.expectCPEMatches { assert.True(t, hasCPEMatch, "expected CPE matches for EOL distro") } else { assert.False(t, hasCPEMatch, "did not expect CPE matches") } }) } } ================================================ FILE: grype/matcher/rpm/rhel_eus.go ================================================ package rpm import ( "fmt" "regexp" "strconv" "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) // elVersionPattern matches patterns like "el9_5", "el8_10", "el7" in RPM release strings var elVersionPattern = regexp.MustCompile(`\.el(\d+)(?:_(\d+))?`) // extractRHELVersionFromRelease parses RHEL major/minor from release string. // Examples: // // "503.11.1.el9_5" -> (9, 5, true) // "82.el8_10.2" -> (8, 10, true) // "1.el7" -> (7, 0, true) // missing minor treated as 0 // "1.0.0" -> (0, 0, false) func extractRHELVersionFromRelease(release string) (major, minor int, found bool) { matches := elVersionPattern.FindStringSubmatch(release) if matches == nil { return 0, 0, false } major, err := strconv.Atoi(matches[1]) if err != nil { return 0, 0, false } // If no minor version captured, treat as 0 (base version) if matches[2] == "" { return major, 0, true } minor, err = strconv.Atoi(matches[2]) if err != nil { return major, 0, true } return major, minor, true } // extractReleaseFromRPMVersion extracts release portion from full RPM version. // Examples: // // "5.14.0-503.11.1.el9_5" -> "503.11.1.el9_5" // "0:5.14.0-503.11.1.el9_5" -> "503.11.1.el9_5" (handles epoch) func extractReleaseFromRPMVersion(rpmVersion string) string { // Strip epoch if present (e.g., "0:5.14.0-..." -> "5.14.0-...") if idx := strings.Index(rpmVersion, ":"); idx != -1 { rpmVersion = rpmVersion[idx+1:] } // Extract release portion after the hyphen if idx := strings.LastIndex(rpmVersion, "-"); idx != -1 { return rpmVersion[idx+1:] } return rpmVersion } // isFixReachableForEUS returns true if fix version is reachable for EUS distro. // A fix is reachable if: // 1. The fix's RHEL version cannot be determined (fail-open) // 2. The fix's major version matches AND minor version <= EUS minor version func isFixReachableForEUS(fixVersion string, eusDistro *distro.Distro) bool { if eusDistro == nil { return true } // Parse EUS distro version (e.g., "9.4" -> major=9, minor=4) eusMajor, eusMinor := eusDistro.MajorVersion(), eusDistro.MinorVersion() if eusMajor == "" { // Cannot determine EUS version, fail-open return true } eusMajorInt, err := strconv.Atoi(eusMajor) if err != nil { return true } // Treat missing minor version as 0 (e.g., "9+eus" means "9.0+eus") eusMinorInt := 0 if eusMinor != "" { eusMinorInt, _ = strconv.Atoi(eusMinor) } // Extract release from fix version and parse RHEL version from it release := extractReleaseFromRPMVersion(fixVersion) fixMajor, fixMinor, found := extractRHELVersionFromRelease(release) if !found { // Cannot determine fix's RHEL version, fail-open (assume reachable) return true } // Different major version is not reachable if fixMajor != eusMajorInt { return false } // Fix is reachable only if fix's minor version <= EUS minor version // (missing minor is treated as 0, so ".el9" is reachable by any EUS version) return fixMinor <= eusMinorInt } func shouldUseRedhatEUSMatching(d *distro.Distro) bool { if d == nil { return false } if d.Type != distro.RedHat { // considering EUS fixes on a non-RedHat distro is not valid return false } for _, channel := range d.Channels { if strings.ToLower(channel) == "eus" { // if the distro has an EUS channel, we should consider EUS fixes return true } } return false } // redhatEUSMatches returns matches for the given package with Extended Update Support (EUS) fixes considered. // // RedHat follows the below workflow when incorporating patches: // // RHEL 9 ───▶ 9.1 ───▶ 9.2 ───▶ 9.2-EUS (mainline + 9.2 EUS fixes) // │ // ▼ // 9.3 ───▶ 9.4 ───▶ 9.4-EUS (mainline + 9.4 EUS fixes) // │ // ▼ // 9.5 ───▶ 9.6 ───▶ 9.6-EUS (mainline + 9.6 EUS fixes) // │ // ▼ ... // // So... // - EUS branches are independent (no cross-EUS fixes) // - each EUS branch = mainline fixes up to branch point + its own EUS fixes // // In grype that means that searching for vulnerabilities should be done in two steps: // 1. find disclosures that match the base distro (e.g., '>= 9.0 && < 10'). // 2. find fixes from the base distro (e.g., '>= 9.0 && < 10') as well as EUS fixes for the specific minor version of the distro (e.g. '9.4+eus'). // // Once searching is complete, we have two collections (matching for each search step above). // We then merge these two (disclosure and resolution) collections together, the final result is a collection of // prototype matches that the package is vulnerable to that include both the base distro disclosures and the EUS fixes. // Any disclosure that does not apply to the original package version (e.g. a fix was found) at this point has been removed. // // The final step is to render the final matches from the merged collection. func redhatEUSMatches(provider result.Provider, searchPkg pkg.Package, missingEpochStrategy version.MissingEpochStrategy) ([]match.Match, error) { distroWithoutEUS := *searchPkg.Distro distroWithoutEUS.Channels = nil // clear the EUS channel so that we can search for the base distro // Create version with config embedded pkgVersion := version.NewWithConfig( searchPkg.Version, pkg.VersionFormat(searchPkg), version.ComparisonConfig{ MissingEpochStrategy: missingEpochStrategy, }, ) // find all disclosures for the package in the base distro (e.g. '>= 9.0 && < 10') disclosures, err := provider.FindResults( search.ByPackageName(searchPkg.Name), search.ByDistro(distroWithoutEUS), // e.g. >= 9.0 && < 10 (no EUS channel) internal.OnlyQualifiedPackages(searchPkg), internal.OnlyVulnerableVersions(pkgVersion), // if these records indicate the version of the package is not vulnerable, do not include them ) if err != nil { return nil, fmt.Errorf("matcher failed to fetch disclosures for distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err) } if len(disclosures) == 0 { return nil, nil } // find all base distro fixes (e.g. '>= 9.0 && < 10') and EUS fixes for the package in the specific minor version of the distro (e.g. '9.4+eus') resolutions, err := provider.FindResults( search.ByPackageName(searchPkg.Name), search.ByDistro(distroWithoutEUS, *searchPkg.Distro), // e.g. (>= 9.0 && < 10) || 9.4+eus internal.OnlyQualifiedPackages(searchPkg), // note: we do **not** apply any version criteria to the search as to raise up all possible fixes // and combine within the collection. If we do filter on a fix version, it could result in // false positives (missing EUS fixes that resolve a disclosure). ) if err != nil { return nil, fmt.Errorf("matcher failed to fetch resolutions for distro=%q pkg=%q: %w", searchPkg.Distro, searchPkg.Name, err) } eusFixes := resolutions.Filter(search.ByFixedVersion(*pkgVersion)) // remove EUS fixed vulns for this version remaining := disclosures.Remove(eusFixes) // combine disclosures and fixes so that: // a. disclosures that have EUS fixes that resolve the disclosure for an earlier version of the package (thus we're not vulnerable) are removed. // b. disclosures that have EUS fixes that resolve the disclosure for future versions of the package (thus we're vulnerable) are kept. // c. all fixes from the incoming resolutions are patched onto the disclosures in the returned collection, so the // final set of vulnerabilities is a fused set of disclosures and fixes together. // Note: we pass searchPkg.Distro (the EUS distro) to filter out fixes not reachable for this EUS version remaining = remaining.Merge(resolutions, mergeEUSAdvisoriesIntoMainDisclosures(pkgVersion, searchPkg.Distro)) return remaining.ToMatches(), err } // mergeEUSAdvisoriesIntoMainDisclosures returns a function that will filter disclosures based on the provided advisory information (by fix version only). // Additionally, this will merge applicable fixes into one vulnerability record, so that the final result contains only one vulnerability record per disclosure. func mergeEUSAdvisoriesIntoMainDisclosures(v *version.Version, eusDistro *distro.Distro) func(disclosures, advisoryOverlays []result.Result) []result.Result { return func(disclosures, advisoryOverlays []result.Result) []result.Result { var out []result.Result for _, ds := range disclosures { processedResult := mergeEUSAdvisoryIntoMainDisclosure(v, ds, advisoryOverlays, eusDistro) if len(processedResult.Vulnerabilities) > 0 { out = append(out, processedResult) } } return out } } // mergeEUSAdvisoryIntoMainDisclosure processes a single disclosure Result against its corresponding advisory overlay Results func mergeEUSAdvisoryIntoMainDisclosure(v *version.Version, disclosures result.Result, advisoryOverlays []result.Result, eusDistro *distro.Distro) result.Result { processedResult := result.Result{ ID: disclosures.ID, Package: disclosures.Package, } // process each disclosure vulnerability against advisory overlays for _, disclosure := range disclosures.Vulnerabilities { processedVuln, advisoryDetails := mergeEUSAdvisoryIntoSingleDisclosure(v, disclosure, advisoryOverlays, eusDistro) if processedVuln != nil { processedResult.Vulnerabilities = append(processedResult.Vulnerabilities, *processedVuln) processedResult.Details = append(processedResult.Details, advisoryDetails...) } } finalizeMatchDetails(&processedResult, disclosures.Details, v) return processedResult } // mergeEUSAdvisoryIntoSingleDisclosure processes a single vulnerability against advisory overlays func mergeEUSAdvisoryIntoSingleDisclosure(v *version.Version, disclosure vulnerability.Vulnerability, advisoryOverlays []result.Result, eusDistro *distro.Distro) (*vulnerability.Vulnerability, match.Details) { fixVersions := version.NewSet(true) var constraints []version.Constraint var state vulnerability.FixState var allAdvisoryDetails match.Details // check if we're vulnerable to the original disclosure if isVulnerableVersion(v, disclosure.Constraint, disclosure.ID) { constraints = append(constraints, disclosure.Constraint) } // process advisory overlays, incorporating new fix versions and updating the version constraints for _, advisoryOverlay := range advisoryOverlays { collectMatchingConstraintsDetailsAndFixState(v, advisoryOverlay, fixVersions, &constraints, &state, &allAdvisoryDetails, eusDistro) } if len(constraints) == 0 { // all of the advisories showed we're not vulnerable, so we can skip this disclosure return nil, nil } patchedRecord := buildPatchedVulnerabilityRecord(v, disclosure, fixVersions, constraints, state) return &patchedRecord, allAdvisoryDetails } // collectMatchingConstraintsDetailsAndFixState processes vulnerabilities from advisory overlays, applying any new fix versions and updating the given fix state / constraints. func collectMatchingConstraintsDetailsAndFixState(v *version.Version, advisoryResult result.Result, fixVersions *version.Set, constraints *[]version.Constraint, state *vulnerability.FixState, allAdvisoryDetails *match.Details, eusDistro *distro.Distro) { advisories := advisoryResult.Vulnerabilities var keepDetails bool for _, advisory := range advisories { if advisory.Fix.State == vulnerability.FixStateWontFix && *state != vulnerability.FixStateFixed { *state = advisory.Fix.State } // Get all fixes greater than current version (parses versions once) allFixes := neededFixes(v, advisory.Fix.Versions, advisory.Constraint.Format(), advisory.ID) // Filter to only those reachable for EUS (quick string-based check) applicableFixes := filterFixesForEUS(allFixes, eusDistro, advisory.ID) if len(applicableFixes) == 0 { // If there were fixes but none are reachable for EUS, mark as NotFixed if len(allFixes) > 0 && eusDistro != nil && *state != vulnerability.FixStateFixed { *state = vulnerability.FixStateNotFixed // Still add the constraint since the user is vulnerable *constraints = append(*constraints, advisory.Constraint) keepDetails = true } // none of the fixes on this advisory are greater than the current version (or reachable for EUS), so we can skip adding fixes continue } // we're vulnerable! keep any fix versions that could have been applied *constraints = append(*constraints, advisory.Constraint) fixVersions.Add(applicableFixes...) if *state != vulnerability.FixStateFixed { *state = advisory.Fix.State } keepDetails = true } // collect details from the advisory overlay only if we kept any of the advisory details if keepDetails && len(advisoryResult.Details) > 0 { *allAdvisoryDetails = append(*allAdvisoryDetails, advisoryResult.Details...) } } // buildPatchedVulnerabilityRecord creates the final patched vulnerability record from the original disclosure and fix/constraint information from applicable advisories. func buildPatchedVulnerabilityRecord(v *version.Version, disclosure vulnerability.Vulnerability, fixVersions *version.Set, constraints []version.Constraint, state vulnerability.FixState) vulnerability.Vulnerability { patchedRecord := disclosure if state == vulnerability.FixStateFixed { patchedRecord.Fix.Versions = nil for _, fixVersion := range fixVersions.Values() { patchedRecord.Fix.Versions = append(patchedRecord.Fix.Versions, fixVersion.Raw) fixConstraint, err := version.GetConstraint(fmt.Sprintf("< %s", fixVersion.Raw), v.Format) if err != nil { log.WithFields("vulnerability", disclosure.ID, "fixVersion", fixVersion, "error", err).Trace("failed to create constraint for fix version") continue // skip this fix version if we cannot create a constraint } constraints = append(constraints, fixConstraint) } } patchedRecord.Fix.State = finalizeFixState(disclosure, state) patchedRecord.Constraint = version.CombineConstraints(constraints...) return patchedRecord } // finalizeMatchDetails patches the processed result details with that of details in the post-processed result. func finalizeMatchDetails(processedResult *result.Result, originalDetails match.Details, v *version.Version) { if len(processedResult.Vulnerabilities) == 0 { return } // keep details around only if we have vulnerabilities they describe processedResult.Details = append(processedResult.Details, originalDetails...) processedResult.Details = result.NewMatchDetailsSet(processedResult.Details...).ToSlice() // patch the version in the details if it is missing for idx := range processedResult.Details { d := &processedResult.Details[idx] switch params := d.SearchedBy.(type) { case match.CPEParameters: if params.Package.Version == "" { params.Package.Version = v.Raw d.SearchedBy = params } case match.DistroParameters: if params.Package.Version == "" { params.Package.Version = v.Raw d.SearchedBy = params } case match.EcosystemParameters: if params.Package.Version == "" { params.Package.Version = v.Raw d.SearchedBy = params } } } } func isVulnerableVersion(v *version.Version, c version.Constraint, id string) bool { if c == nil { // nil constraint is different than an empty constraint, so we should not consider this vulnerable return false } isVulnerable, err := c.Satisfied(v) if err != nil { log.WithFields("vulnerability", id, "error", err).Trace("failed to check constraint") return false // if we cannot determine if the version is vulnerable, we assume it is not } return isVulnerable } func neededFixes(v *version.Version, fixVersions []string, format version.Format, id string) []*version.Version { var needed []*version.Version for _, fixVersion := range fixVersions { fixVersionObj := version.New(fixVersion, format) // note: we use the format from the advisory, not the version itself res, err := v.Is(version.LT, fixVersionObj) if err != nil { log.WithFields("format", format, "version", fixVersion, "error", err, "vulnerability", id).Trace("failed to evaluate fix version") continue } if res { needed = append(needed, fixVersionObj) } } return needed } // filterFixesForEUS filters a list of fix versions to only those reachable for the given EUS distro. func filterFixesForEUS(fixes []*version.Version, eusDistro *distro.Distro, id string) []*version.Version { if eusDistro == nil { return fixes } var reachable []*version.Version for _, fix := range fixes { if isFixReachableForEUS(fix.Raw, eusDistro) { reachable = append(reachable, fix) } else { log.WithFields("vulnerability", id, "fixVersion", fix.Raw, "eusDistro", eusDistro.String()).Trace("skipping fix not reachable for EUS version") } } return reachable } func finalizeFixState(record vulnerability.Vulnerability, state vulnerability.FixState) vulnerability.FixState { if state == "" { state = vulnerability.FixStateUnknown } if state != vulnerability.FixStateUnknown { return state } if record.Fix.State != vulnerability.FixStateUnknown { return record.Fix.State } return vulnerability.FixStateUnknown } ================================================ FILE: grype/matcher/rpm/rhel_eus_test.go ================================================ package rpm import ( "errors" "sort" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal/result" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestExtractRHELVersionFromRelease(t *testing.T) { tests := []struct { name string release string wantMajor int wantMinor int wantFound bool }{ { name: "el9_5 pattern", release: "503.11.1.el9_5", wantMajor: 9, wantMinor: 5, wantFound: true, }, { name: "el8_10 pattern (double digit minor)", release: "82.el8_10.2", wantMajor: 8, wantMinor: 10, wantFound: true, }, { name: "el7 pattern (no minor version treated as 0)", release: "1.el7", wantMajor: 7, wantMinor: 0, wantFound: true, }, { name: "el9 pattern (no minor version treated as 0)", release: "427.79.1.el9", wantMajor: 9, wantMinor: 0, wantFound: true, }, { name: "el9_4 pattern", release: "427.79.1.el9_4", wantMajor: 9, wantMinor: 4, wantFound: true, }, { name: "no el pattern", release: "1.0.0", wantMajor: 0, wantMinor: 0, wantFound: false, }, { name: "empty string", release: "", wantMajor: 0, wantMinor: 0, wantFound: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotMajor, gotMinor, gotFound := extractRHELVersionFromRelease(tt.release) assert.Equal(t, tt.wantMajor, gotMajor, "major version mismatch") assert.Equal(t, tt.wantMinor, gotMinor, "minor version mismatch") assert.Equal(t, tt.wantFound, gotFound, "found mismatch") }) } } func TestExtractReleaseFromRPMVersion(t *testing.T) { tests := []struct { name string rpmVersion string want string }{ { name: "version with hyphen", rpmVersion: "5.14.0-503.11.1.el9_5", want: "503.11.1.el9_5", }, { name: "version with epoch and hyphen", rpmVersion: "0:5.14.0-503.11.1.el9_5", want: "503.11.1.el9_5", }, { name: "version without hyphen", rpmVersion: "1.0.0", want: "1.0.0", }, { name: "version with epoch but no hyphen", rpmVersion: "1:2.3.4", want: "2.3.4", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractReleaseFromRPMVersion(tt.rpmVersion) assert.Equal(t, tt.want, got) }) } } func TestIsFixReachableForEUS(t *testing.T) { tests := []struct { name string fixVersion string eusDistro *distro.Distro want bool }{ { name: "nil distro - always reachable", fixVersion: "5.14.0-503.11.1.el9_5", eusDistro: nil, want: true, }, { name: "el9_5 fix NOT reachable from 9.4 EUS", fixVersion: "5.14.0-503.11.1.el9_5", eusDistro: newEUSDistro("9.4"), want: false, }, { name: "el9_4 fix IS reachable from 9.4 EUS (same minor)", fixVersion: "5.14.0-427.80.1.el9_4", eusDistro: newEUSDistro("9.4"), want: true, }, { // This is the key test case: mainline 9.2 fixes ARE reachable from 9.4 EUS // because EUS 9.4 includes all mainline fixes up to 9.4 name: "el9_2 mainline fix IS reachable from 9.4 EUS (lower minor version)", fixVersion: "5.14.0-100.el9_2", eusDistro: newEUSDistro("9.4"), want: true, }, { name: "el9 base fix IS reachable from 9.4 EUS (no minor version in fix)", fixVersion: "5.14.0-100.el9", eusDistro: newEUSDistro("9.4"), want: true, }, { name: "el8 fix NOT reachable from el9 (different major)", fixVersion: "4.18.0-100.el8_10", eusDistro: newEUSDistro("9.4"), want: false, }, { name: "no el pattern - fail open (assume reachable)", fixVersion: "1.0.0-1", eusDistro: newEUSDistro("9.4"), want: true, }, { name: "distro without version - fail open", fixVersion: "5.14.0-503.11.1.el9_5", eusDistro: newEUSDistro(""), want: true, }, { // "9+eus" (no minor) is treated as "9.0+eus", so el9_1 fixes are NOT reachable name: "distro with major only treated as .0 - el9_1 fix NOT reachable from 9+eus", fixVersion: "5.14.0-100.el9_1", eusDistro: distro.New(distro.RedHat, "9+eus", ""), want: false, }, { // "9+eus" (no minor) is treated as "9.0+eus", so el9_0 fixes ARE reachable name: "distro with major only treated as .0 - el9_0 fix IS reachable from 9+eus", fixVersion: "5.14.0-100.el9_0", eusDistro: distro.New(distro.RedHat, "9+eus", ""), want: true, }, { // "9+eus" (no minor) is treated as "9.0+eus", so base el9 fixes ARE reachable name: "distro with major only treated as .0 - el9 base fix IS reachable from 9+eus", fixVersion: "5.14.0-100.el9", eusDistro: distro.New(distro.RedHat, "9+eus", ""), want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isFixReachableForEUS(tt.fixVersion, tt.eusDistro) assert.Equal(t, tt.want, got) }) } } func TestResolveEUSDisclosures(t *testing.T) { tests := []struct { name string packageVersion string disclosures []result.Result advisoryOverlay []result.Result want []result.Result }{ { name: "disclosure with fix version - package version is vulnerable", packageVersion: "1.0.0", // vulnerable since 1.0.0 < 1.5.0 disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.6.0", version.RpmFormat), // important! Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{"1.6.0"}, // important! this is the fix version that we should not consider }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), // important! Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, // important! Versions: []string{"1.5.0"}, // important! }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.CombineConstraints( version.MustGetConstraint("< 1.6.0", version.RpmFormat), // from disclosure version.MustGetConstraint("< 1.5.0", version.RpmFormat), // from advisory ), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, // important! from advisory Versions: []string{"1.5.0"}, // important! from advisory, not the disclosure }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, { name: "vulnerability not fixed - package version not vulnerable", packageVersion: "2.0.0", // not vulnerable since 2.0.0 > 1.5.0 disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), // important! Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), // important! Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{}, }, { name: "multiple advisories with multiple fix versions", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { // advisory does not apply! Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 0.9", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"0.9"}, }, }, { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0", "1.4.2"}, }, }, { // duplicate advisory should already be counted from the first one Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0", "1.4.2"}, }, }, { // duplicate advisory, with a different fix version Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.4.3"}, }, }, { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"2.0.0"}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.CombineConstraints( // important! we are combining the constraints version.MustGetConstraint("< 1.5.0", version.RpmFormat), version.MustGetConstraint("< 2.0.0", version.RpmFormat), version.MustGetConstraint("< 1.4.2", version.RpmFormat), version.MustGetConstraint("< 1.4.3", version.RpmFormat), ), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.4.2", "1.4.3", "1.5.0", "2.0.0"}, // important! we have all fixes for advisories that apply }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, { name: "advisory with wont-fix state - disclosure should be kept with patched fix state", packageVersion: "1.0.0", // vulnerable since 1.0.0 < 2.0.0 disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, // important! the disclosure doesn't have good fix info Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateWontFix, // important! we want the match to reflect this property Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateWontFix, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, { name: "advisory with unknown fix state - disclosure should be kept", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), // important! Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { // ultimately, this advisory does not apply... ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 3.0.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, // important! Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 2.0.0", version.RpmFormat), // from the disclosure (nothing from the resolution since there was no fix information) Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, { name: "empty fix versions are filtered out", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"", "1.5.0", ""}, // important! }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, // note: empty versions are filtered out }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, { name: "constraint satisfaction error - advisory skipped", packageVersion: "W:1.2.3-456", // intentionally invalid epoch (will fail to parse) disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{}, }, { name: "no advisory overlay, disclosure has nil constraint - remove disclosure", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: nil, // important! we're never vulnerable! Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { // does not apply ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{}, Details: []match.Detail{}, }, }, want: []result.Result{}, }, { name: "no advisory overlay, disclosure has empty constraint - keep disclosure", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("", version.RpmFormat), // important! we're always vulnerable Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { // does not apply ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{}, Details: []match.Detail{}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("", version.RpmFormat), // important! shows "none (rpm)" Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, { name: "no advisory overlay, disclosure does not apply - remove all", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 0.9", version.RpmFormat), // important! we're not vulnerable! Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { // does not apply ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{}, Details: []match.Detail{}, }, }, want: []result.Result{}, }, { name: "advisory with no fixes - disclosure is preserved", packageVersion: "1.0.0", disclosures: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, advisoryOverlay: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateWontFix, Versions: []string{"1.5.0"}, // important: this is a wont-fix advisory so this should not be incorporated (an inconsistent advisory) }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, want: []result.Result{ { ID: "CVE-2021-1", Vulnerabilities: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ID: "CVE-2021-1"}, Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateWontFix, // wont-fix state is preserved Versions: []string{}, }, }, }, Details: []match.Detail{{Type: match.ExactDirectMatch}}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var v *version.Version v = version.New(tt.packageVersion, version.RpmFormat) if v.Validate() != nil { v = nil } resolver := mergeEUSAdvisoriesIntoMainDisclosures(v, nil) got := resolver(tt.disclosures, tt.advisoryOverlay) opts := cmp.Options{ cmpopts.IgnoreUnexported(result.Result{}), cmpopts.IgnoreUnexported(version.Version{}), cmpopts.EquateEmpty(), } if diff := cmp.Diff(tt.want, got, opts...); diff != "" { t.Errorf("mergeEUSAdvisoriesIntoMainDisclosures() mismatch (-want +got):\n%s", diff) } }) } } func TestRedhatEUSMatches(t *testing.T) { testPkg1 := pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), } tests := []struct { name string catalogPkg pkg.Package searchPkg *pkg.Package disclosureVulns []vulnerability.Vulnerability resolutionVulns []vulnerability.Vulnerability disclosureError error resolutionError error want []match.Match wantErr require.ErrorAssertionFunc }{ { name: "empty set of disclosures and advisories", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{}, resolutionVulns: []vulnerability.Vulnerability{}, want: nil, }, { name: "successful EUS match with fix - direct match", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // same as searched package = direct match Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // same as searched package = direct match Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, }, want: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, Package: pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, Details: []match.Detail{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "< 1.5.0 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4+eus", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "< 1.5.0 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, }, }, }, }, { name: "successful EUS match with fix - indirect match", catalogPkg: testPkg1, searchPkg: &pkg.Package{ ID: pkg.ID("indirect-test-pkg-id"), Name: "indirect-test-pkg", // important! this will be detected as an indirect match Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "indirect-test-pkg", // setup to match search package name Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "indirect-test-pkg", // setup to match search package name Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, }, want: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "indirect-test-pkg", Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, Package: pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, Details: []match.Detail{ { Type: match.ExactIndirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "indirect-test-pkg", // important! we used the indirect package as input Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "< 1.5.0 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, { Type: match.ExactIndirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4+eus", }, Package: match.PackageParameter{ Name: "indirect-test-pkg", // important! we used the indirect package as input Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "< 1.5.0 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, }, }, }, }, { name: "valid disclosures found but no resolutions", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("", version.RpmFormat), // no constraint, so always vulnerable Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{}, want: []match.Match{ // keep the original disclosure as a match { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", Constraint: version.MustGetConstraint("", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, Package: pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, Details: []match.Detail{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "none (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, }, }, }, }, { name: "vulnerability resolved by EUS advisory", catalogPkg: pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "2.0.0", // version higher than fix, so resolved Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, }, want: []match.Match{}, // vulnerability is resolved because package version 2.0.0 > 1.5.0 }, { name: "multiple valid disclosures with mixed resolutions", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, { Reference: vulnerability.Reference{ ID: "CVE-2021-2", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, { Reference: vulnerability.Reference{ ID: "CVE-2021-3", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: nil, // no constraint, so we assume we're never vulnerable to this Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, }, want: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, Package: pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, Details: []match.Detail{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "< 1.5.0 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4+eus", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "< 1.5.0 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-1", VersionConstraint: "none (rpm)", // important! this is the disclosure with no constraint }, Matcher: match.RpmMatcher, Confidence: 1, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2021-2", Namespace: "namespace", }, PackageName: "test-pkg", Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, Package: pkg.Package{ ID: pkg.ID("test-pkg-id"), Name: "test-pkg", Version: "1.0.0", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, Details: []match.Detail{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "test-pkg", Version: "1.0.0", }, Namespace: "namespace", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2021-2", VersionConstraint: "none (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, }, }, }, }, { name: "multiple advisories with mixed fix state relative to search package", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("< 1.5.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.5.0"}, }, }, { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Constraint: version.MustGetConstraint("< 1.0.0", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"1.0.0"}, }, }, }, want: []match.Match{}, }, { name: "error fetching disclosures", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{}, resolutionVulns: []vulnerability.Vulnerability{}, disclosureError: errors.New("disclosure error"), want: nil, wantErr: require.Error, }, { // This test case demonstrates issue #2847: when a user is on RHEL 9.4+eus and // the only available fix is for RHEL 9.5 (indicated by el9_5 in the fix version), // the vulnerability should NOT be reported as "Fixed" when using --only-fixed, // because the EUS user cannot upgrade to RHEL 9.5. name: "fix version for higher minor version should not be considered fixed for EUS - issue 2847", catalogPkg: pkg.Package{ ID: pkg.ID("kernel-id"), Name: "kernel", Version: "5.14.0-427.79.1.el9_4", // user's current version on RHEL 9.4 EUS Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2020-10135", Namespace: "redhat:distro:redhat:9", }, PackageName: "kernel", Constraint: version.MustGetConstraint("< 5.14.0-503.11.1.el9_5", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{ { // This fix is for RHEL 9.5 (indicated by el9_5 in the version), // which is NOT available to RHEL 9.4 EUS users Reference: vulnerability.Reference{ ID: "CVE-2020-10135", Namespace: "redhat:distro:redhat:9", }, PackageName: "kernel", Constraint: version.MustGetConstraint("< 5.14.0-503.11.1.el9_5", version.RpmFormat), Fix: vulnerability.Fix{ State: vulnerability.FixStateFixed, Versions: []string{"5.14.0-503.11.1.el9_5"}, // note: el9_5 indicates RHEL 9.5 }, }, }, // Expected behavior: since the fix requires upgrading to RHEL 9.5 and the user // is on RHEL 9.4 EUS (can't upgrade to 9.5), the fix should NOT be considered // valid and the FixState should be NotFixed (not Fixed). want: []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2020-10135", Namespace: "redhat:distro:redhat:9", }, PackageName: "kernel", Fix: vulnerability.Fix{ State: vulnerability.FixStateNotFixed, // fix exists but not reachable for EUS 9.4 Versions: []string{}, // no valid fixes for EUS 9.4 }, }, Package: pkg.Package{ ID: pkg.ID("kernel-id"), Name: "kernel", Version: "5.14.0-427.79.1.el9_4", Type: syftPkg.RpmPkg, Distro: newEUSDistro("9.4"), }, Details: []match.Detail{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4", }, Package: match.PackageParameter{ Name: "kernel", Version: "5.14.0-427.79.1.el9_4", }, Namespace: "redhat:distro:redhat:9", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-10135", VersionConstraint: "< 5.14.0-503.11.1.el9_5 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "redhat", Version: "9.4+eus", }, Package: match.PackageParameter{ Name: "kernel", Version: "5.14.0-427.79.1.el9_4", }, Namespace: "redhat:distro:redhat:9", }, Found: match.DistroResult{ VulnerabilityID: "CVE-2020-10135", VersionConstraint: "< 5.14.0-503.11.1.el9_5 (rpm)", }, Matcher: match.RpmMatcher, Confidence: 1, }, }, }, }, }, { name: "error fetching resolutions", catalogPkg: testPkg1, disclosureVulns: []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2021-1", Namespace: "namespace", }, PackageName: "test-pkg", // direct match Fix: vulnerability.Fix{ State: vulnerability.FixStateUnknown, Versions: []string{}, }, }, }, resolutionVulns: []vulnerability.Vulnerability{}, resolutionError: errors.New("resolution error"), want: nil, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } if tt.searchPkg == nil { tt.searchPkg = &tt.catalogPkg } vulnProvider := newMockVulnProvider() vulnProvider.setDisclosureVulns(tt.disclosureVulns) vulnProvider.setResolutionVulns(tt.resolutionVulns) vulnProvider.setDisclosureError(tt.disclosureError) vulnProvider.setResolutionError(tt.resolutionError) resultProvider := result.NewProvider(vulnProvider, tt.catalogPkg, match.RpmMatcher) got, err := redhatEUSMatches(resultProvider, *tt.searchPkg, "zero") tt.wantErr(t, err) if err != nil { return } // need stable results for comparison sort.Sort(match.ByElements(got)) opts := cmp.Options{ cmpopts.IgnoreUnexported(version.Version{}), cmpopts.IgnoreUnexported(distro.Distro{}), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.IgnoreFields(pkg.Package{}, "Locations"), cmpopts.EquateEmpty(), } if diff := cmp.Diff(tt.want, got, opts...); diff != "" { t.Errorf("redhatEUSMatches() mismatch (-want +got):\n%s", diff) } }) } } func strRef(s string) *string { return &s } func intRef(s int) *int { return &s } // Mock vulnerability provider for testing type mockVulnProvider struct { // cheaply get a working interface that will panic when functionality is not overridden vulnerability.Provider disclosureVulns []vulnerability.Vulnerability resolutionVulns []vulnerability.Vulnerability disclosureError error resolutionError error callCount int } func newMockVulnProvider() *mockVulnProvider { return &mockVulnProvider{} } func (m *mockVulnProvider) setDisclosureVulns(vulns []vulnerability.Vulnerability) { m.disclosureVulns = vulns } func (m *mockVulnProvider) setResolutionVulns(vulns []vulnerability.Vulnerability) { m.resolutionVulns = vulns } func (m *mockVulnProvider) setDisclosureError(err error) { m.disclosureError = err } func (m *mockVulnProvider) setResolutionError(err error) { m.resolutionError = err } func (m *mockVulnProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { m.callCount++ // heuristic: first call is for disclosures (base distro), second is for resolutions (base + eus distro) if m.callCount == 1 { if m.disclosureError != nil { return nil, m.disclosureError } return m.disclosureVulns, nil } if m.resolutionError != nil { return nil, m.resolutionError } return m.resolutionVulns, nil } func channels(s ...string) []string { return s } // newEUSDistro creates a properly initialized RHEL EUS distro using distro.New(). // This ensures MajorVersion()/MinorVersion() work correctly. // Pass version like "9.4" (the "+eus" channel suffix is added automatically). // Pass empty string for a distro without a version. func newEUSDistro(version string) *distro.Distro { if version == "" { // For empty version, we need to set channels manually since "+eus" alone // doesn't parse well d := distro.New(distro.RedHat, "", "") d.Channels = []string{"eus"} return d } return distro.New(distro.RedHat, version+"+eus", "") } ================================================ FILE: grype/matcher/ruby/matcher.go ================================================ package ruby import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewRubyMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.GemPkg} } func (m *Matcher) Type() match.MatcherType { return match.RubyGemMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/rust/matcher.go ================================================ package rust import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewRustMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return []syftPkg.Type{syftPkg.RustPkg} } func (m *Matcher) Type() match.MatcherType { return match.RustMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/stock/matcher.go ================================================ package stock import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/internal" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) type Matcher struct { cfg MatcherConfig } type MatcherConfig struct { UseCPEs bool } func NewStockMatcher(cfg MatcherConfig) match.Matcher { return &Matcher{ cfg: cfg, } } func (m *Matcher) PackageTypes() []syftPkg.Type { return nil } func (m *Matcher) Type() match.MatcherType { return match.StockMatcher } func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs) } ================================================ FILE: grype/matcher/stock/matcher_test.go ================================================ package stock import ( "testing" "github.com/google/uuid" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestMatcher_JVMPackage(t *testing.T) { p := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "java_se", Version: "1.8.0_400", Type: syftPkg.BinaryPkg, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:oracle:java_se:1.8.0:update400:*:*:*:*:*:*", cpe.DeclaredSource), }, } matcher := Matcher{ cfg: MatcherConfig{ UseCPEs: true, }, } store := newMockProvider() actual, _, err := matcher.Match(store, p) require.NoError(t, err) foundCVEs := strset.New() for _, v := range actual { foundCVEs.Add(v.Vulnerability.ID) require.NotEmpty(t, v.Details) for _, d := range v.Details { assert.Equal(t, match.CPEMatch, d.Type, "indirect match not indicated") assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type") } assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name") } expected := strset.New( "CVE-2024-20919-real", "CVE-2024-20919-bonkers-format", "CVE-2024-20919-post-jep223", ) for _, id := range expected.List() { if !foundCVEs.Has(id) { t.Errorf("missing CVE: %s", id) } } extra := strset.Difference(foundCVEs, expected) for _, id := range extra.List() { t.Errorf("unexpected CVE: %s", id) } if t.Failed() { t.Logf("discovered CVES: %d", foundCVEs.Size()) for _, id := range foundCVEs.List() { t.Logf(" - %s", id) } } } func newMockProvider() vulnerability.Provider { // derived from vuln data found on CVE-2024-20919 hit := "< 1.8.0_401 || >= 1.9-ea, < 8.0.401 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2" cpes := []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:java_se:*:*:*:*:*:*:*:*", "")} return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { // positive cases PackageName: "java_se", Constraint: version.MustGetConstraint(hit, version.JVMFormat), Reference: vulnerability.Reference{ID: "CVE-2024-20919-real", Namespace: "nvd:cpe"}, CPEs: cpes, }, { // positive cases PackageName: "java_se", Constraint: version.MustGetConstraint("< 22.22.22", version.UnknownFormat), Reference: vulnerability.Reference{ID: "CVE-2024-20919-bonkers-format", Namespace: "nvd:cpe"}, CPEs: cpes, }, { // negative case PackageName: "java_se", Constraint: version.MustGetConstraint("< 1.8.0_399 || >= 1.9-ea, < 8.0.399 || >= 9-ea", version.JVMFormat), Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-update", Namespace: "nvd:cpe"}, CPEs: cpes, }, { // positive case PackageName: "java_se", Constraint: version.MustGetConstraint("< 8.0.401", version.JVMFormat), Reference: vulnerability.Reference{ID: "CVE-2024-20919-post-jep223", Namespace: "nvd:cpe"}, CPEs: cpes, }, { // negative case PackageName: "java_se", Constraint: version.MustGetConstraint("< 8.0.399", version.JVMFormat), Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223", Namespace: "nvd:cpe"}, CPEs: cpes, }, { // negative case PackageName: "java_se", Constraint: version.MustGetConstraint("< 7.0.0", version.JVMFormat), Reference: vulnerability.Reference{ID: "CVE-FAKE-bad-range-post-jep223", Namespace: "nvd:cpe"}, CPEs: cpes, }, }...) } ================================================ FILE: grype/pkg/apk_metadata.go ================================================ package pkg import ( "sort" "github.com/scylladb/go-set/strset" ) var _ FileOwner = (*ApkMetadata)(nil) type ApkMetadata struct { Files []ApkFileRecord `json:"files"` } // ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records). type ApkFileRecord struct { Path string `json:"path"` } func (m ApkMetadata) OwnedFiles() []string { s := strset.New() for _, f := range m.Files { if f.Path != "" { s.Add(f.Path) } } result := s.List() sort.Strings(result) return result } ================================================ FILE: grype/pkg/context.go ================================================ package pkg import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/source" ) type Context struct { Source *source.Description Distro *distro.Distro // DistroDetectionFailed is true when linux release info was present but // the distro type could not be determined (e.g., unknown distro ID) DistroDetectionFailed bool } ================================================ FILE: grype/pkg/context_test.go ================================================ package pkg import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" ) func TestContext_DistroDetectionFailed(t *testing.T) { tests := []struct { name string ctx Context expected bool }{ { name: "detection failed is false by default", ctx: Context{}, expected: false, }, { name: "detection failed is true when set", ctx: Context{ DistroDetectionFailed: true, }, expected: true, }, { name: "detection failed with distro present", ctx: Context{ Distro: distro.New(distro.Ubuntu, "22.04", "jammy"), DistroDetectionFailed: false, }, expected: false, }, { name: "detection failed with nil distro", ctx: Context{ Distro: nil, DistroDetectionFailed: true, }, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, tt.ctx.DistroDetectionFailed) }) } } ================================================ FILE: grype/pkg/cpe_provider.go ================================================ package pkg import ( "fmt" "io" "strings" "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) const cpeInputPrefix = "cpe:" const cpeListPrefix = "cpes:" type CPELiteralMetadata struct { CPE string } func cpeProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { reader, ctx, err := getCPEReader(userInput) if err != nil { return nil, Context{}, nil, err } s, _, _, err := format.Decode(reader) if s == nil { return nil, Context{}, nil, fmt.Errorf("unable to decode cpe: %w", err) } return FromCollection(s.Artifacts.Packages, config.SynthesisConfig), ctx, s, nil } func getCPEReader(userInput string) (r io.Reader, ctx Context, err error) { if strings.HasPrefix(userInput, cpeInputPrefix) { ctx.Source = &source.Description{ Metadata: CPELiteralMetadata{ CPE: userInput, }, } return strings.NewReader(userInput), ctx, nil } return nil, ctx, errDoesNotProvide } ================================================ FILE: grype/pkg/cpe_provider_test.go ================================================ package pkg import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) func Test_CPEProvider(t *testing.T) { tests := []struct { name string userInput string context Context pkgs []Package sbom *sbom.SBOM wantErr require.ErrorAssertionFunc }{ { name: "takes a single cpe", userInput: "cpe:/a:apache:log4j:2.14.1", context: Context{ Source: &source.Description{ Metadata: CPELiteralMetadata{ CPE: "cpe:/a:apache:log4j:2.14.1", }, }, }, pkgs: []Package{ { Name: "log4j", Version: "2.14.1", CPEs: []cpe.CPE{ cpe.Must("cpe:/a:apache:log4j:2.14.1", ""), }, }, }, sbom: &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(pkg.Package{ Name: "log4j", Version: "2.14.1", CPEs: []cpe.CPE{ cpe.Must("cpe:/a:apache:log4j:2.14.1", ""), }, }), }, }, }, { name: "takes cpe with no version", userInput: "cpe:/a:apache:log4j", context: Context{ Source: &source.Description{ Metadata: CPELiteralMetadata{ CPE: "cpe:/a:apache:log4j", }, }, }, pkgs: []Package{ { Name: "log4j", CPEs: []cpe.CPE{ cpe.Must("cpe:/a:apache:log4j", ""), }, }, }, sbom: &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(pkg.Package{ Name: "log4j", CPEs: []cpe.CPE{ cpe.Must("cpe:/a:apache:log4j", ""), }, }), }, }, }, { name: "takes CPE 2.3 format", userInput: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", context: Context{ Source: &source.Description{ Metadata: CPELiteralMetadata{ CPE: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", }, }, }, pkgs: []Package{ { Name: "log4j", Version: "2.14.1", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""), }, }, }, sbom: &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(pkg.Package{ Name: "log4j", Version: "2.14.1", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""), }, }), }, }, }, { name: "takes multiple CPEs", userInput: `cpe:/a:apache:log4j:2.14.1 cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*`, context: Context{ Source: &source.Description{ Metadata: CPELiteralMetadata{ CPE: `cpe:/a:apache:log4j:2.14.1 cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*`, }, }, }, pkgs: []Package{ { Name: "log4j", Version: "2.14.1", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""), }, }, { Name: "nginx", Version: "", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*", ""), }, }, }, sbom: &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(pkg.Package{ Name: "log4j", Version: "2.14.1", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""), }, }, pkg.Package{ Name: "nginx", Version: "", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*", ""), }, }, ), }, }, }, { name: "invalid prefix", userInput: "dir:testdata/cpe", wantErr: require.Error, }, } opts := []cmp.Option{ cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Metadata", "Type", "Language"), } syftPkgOpts := []cmp.Option{ cmpopts.IgnoreFields(pkg.Package{}, "id", "Type", "Language"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}), } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } packages, ctx, gotSBOM, err := cpeProvider(tc.userInput, ProviderConfig{}) tc.wantErr(t, err) if err != nil { require.Nil(t, packages) return } if d := cmp.Diff(tc.context, ctx, opts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } require.Len(t, packages, len(tc.pkgs)) for idx, expected := range tc.pkgs { if d := cmp.Diff(expected, packages[idx], opts...); d != "" { t.Errorf("unexpected package (-want +got):\n%s", d) } } gotSyftPkgs := gotSBOM.Artifacts.Packages.Sorted() wantSyftPkgs := tc.sbom.Artifacts.Packages.Sorted() require.Equal(t, len(gotSyftPkgs), len(wantSyftPkgs)) for idx, wantPkg := range wantSyftPkgs { if d := cmp.Diff(wantPkg, gotSyftPkgs[idx], syftPkgOpts...); d != "" { t.Errorf("unexpected Syft Package (-want +got):\n%s", d) } } }) } } ================================================ FILE: grype/pkg/file_owner.go ================================================ package pkg type FileOwner interface { OwnedFiles() []string } ================================================ FILE: grype/pkg/golang_metadata.go ================================================ package pkg import "github.com/anchore/syft/syft/pkg" type GolangBinMetadata struct { BuildSettings pkg.KeyValues `json:"goBuildSettings,omitempty" cyclonedx:"goBuildSettings"` GoCompiledVersion string `json:"goCompiledVersion" cyclonedx:"goCompiledVersion"` Architecture string `json:"architecture" cyclonedx:"architecture"` H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"` MainModule string `json:"mainModule,omitempty" cyclonedx:"mainModule"` GoCryptoSettings []string `json:"goCryptoSettings,omitempty" cyclonedx:"goCryptoSettings"` } type GolangModMetadata struct { H1Digest string `json:"h1Digest,omitempty"` } type GolangSourceMetadata struct { H1Digest string `json:"h1Digest,omitempty"` OperatingSystem string `json:"os,omitempty"` Architecture string `json:"architecture,omitempty"` BuildTags string `json:"buildTags,omitempty"` CgoEnabled bool `json:"cgoEnabled"` } ================================================ FILE: grype/pkg/java_metadata.go ================================================ package pkg import ( "github.com/scylladb/go-set/strset" syftPkg "github.com/anchore/syft/syft/pkg" ) type JavaMetadata struct { VirtualPath string `json:"virtualPath"` PomArtifactID string `json:"pomArtifactID"` PomGroupID string `json:"pomGroupID"` ManifestName string `json:"manifestName"` ArchiveDigests []Digest `json:"archiveDigests"` } type Digest struct { Algorithm string `json:"algorithm"` Value string `json:"value"` } type JavaVMInstallationMetadata struct { Release JavaVMReleaseMetadata `json:"release,omitempty"` } type JavaVMReleaseMetadata struct { JavaRuntimeVersion string `json:"javaRuntimeVersion,omitempty"` JavaVersion string `json:"javaVersion,omitempty"` FullVersion string `json:"fullVersion,omitempty"` SemanticVersion string `json:"semanticVersion,omitempty"` } func isJvmPackage(p Package) bool { if _, ok := p.Metadata.(JavaVMInstallationMetadata); ok { return true } if p.Type == syftPkg.BinaryPkg { if HasJvmPackageName(p.Name) { return true } } return false } var jvmIndications = strset.New("java_se", "jre", "jdk", "zulu", "openjdk", "java", "java/jre", "java/jdk") func HasJvmPackageName(name string) bool { return jvmIndications.Has(name) } ================================================ FILE: grype/pkg/java_metadata_test.go ================================================ package pkg import ( "testing" "github.com/stretchr/testify/assert" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestIsJvmPackage(t *testing.T) { tests := []struct { name string pkg Package expected bool }{ { name: "binary package with jdk in name set", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "jdk", }, expected: true, }, { name: "binary package with jre in name set", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "jre", }, expected: true, }, { name: "binary package with java_se in name set", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "java_se", }, expected: true, }, { name: "binary package with zulu in name set", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "zulu", }, expected: true, }, { name: "binary package with openjdk in name set", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "openjdk", }, expected: true, }, { name: "binary package from syft (java/jdk", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "java/jre", }, expected: true, }, { name: "binary package from syft (java/jre)", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "java/jdk", }, expected: true, }, { name: "binary package without jvm-related name", pkg: Package{ Type: syftPkg.BinaryPkg, Name: "nodejs", }, expected: false, }, { name: "non-binary package with jvm-related name", pkg: Package{ Type: syftPkg.NpmPkg, // we know this could not be a JVM package installation Name: "jdk", }, expected: false, }, { name: "package with JavaVMInstallationMetadata", pkg: Package{ Type: syftPkg.RpmPkg, Name: "random-package", Metadata: JavaVMInstallationMetadata{}, }, expected: true, }, { name: "package without JavaVMInstallationMetadata", pkg: Package{ Type: syftPkg.RpmPkg, Name: "non-jvm-package", Metadata: nil, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isJvmPackage(tt.pkg) assert.Equal(t, tt.expected, result) }) } } ================================================ FILE: grype/pkg/package.go ================================================ package pkg import ( "fmt" "regexp" "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) // the source-rpm field has something akin to "util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm" // in which case the pattern will extract out the following values for the named capture groups: // // name = "util-linux-ng" // version = "2.17.2" (or, if there's an epoch, we'd expect a value like "4:2.17.2") // release = "12.28.el6_9.2" // arch = "src" var rpmPackageNamePattern = regexp.MustCompile(`^(?P.*)-(?P.*)-(?P.*)\.(?P[a-zA-Z][^.]+)(\.rpm)$`) // ID represents a unique value for each package added to a package collection. type ID string // Package represents an application or library that has been bundled into a distributable format. type Package struct { ID ID Name string // the package name Version string // the version of the package Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) Language syftPkg.Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) Distro *distro.Distro // a specific distro this package originated from Licenses []string Type syftPkg.Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) CPEs []cpe.CPE // all possible Common Platform Enumerators PURL string // the Package URL (see https://github.com/package-url/purl-spec) Upstreams []UpstreamPackage Metadata interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching } type Enhancer func(out *Package, purl packageurl.PackageURL, pkg syftPkg.Package) func New(p syftPkg.Package, enhancers ...Enhancer) Package { metadata, upstreams := dataFromPkg(p) licenseObjs := p.Licenses.ToSlice() // note: this is used for presentation downstream and is a collection, thus should always be allocated licenses := make([]string, 0, len(licenseObjs)) for _, l := range licenseObjs { licenses = append(licenses, l.Value) } if licenses == nil { licenses = []string{} } out := Package{ ID: ID(p.ID()), Name: p.Name, Version: p.Version, Locations: p.Locations, Licenses: licenses, Language: p.Language, Type: p.Type, CPEs: p.CPEs, PURL: p.PURL, Upstreams: upstreams, Metadata: metadata, } if len(enhancers) > 0 { purl, err := packageurl.FromString(p.PURL) if err != nil { log.WithFields("purl", purl, "error", err).Debug("unable to parse PURL") } for _, e := range enhancers { e(&out, purl, p) } } return out } func FromCollection(catalog *syftPkg.Collection, config SynthesisConfig, enhancers ...Enhancer) []Package { return FromPackages(catalog.Sorted(), config, enhancers...) } func FromPackages(syftPkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package { var pkgs []Package // if the user provided a distro explicitly, then use that over any distro that may be inferred from a package url enhancers = append([]Enhancer{applyDistroOverride(config.Distro.Override)}, enhancers...) for _, p := range syftPkgs { if len(p.CPEs) == 0 { // for SPDX (or any format, really) we may have no CPEs if config.GenerateMissingCPEs { p.CPEs = cpes.Generate(p) } else { log.Debugf("no CPEs for package: %s", p) } } pkgs = append(pkgs, New(p, enhancers...)) } return pkgs } func (p Package) String() string { var d string if p.Distro != nil { d = fmt.Sprintf(", distro=%s", p.Distro.String()) } var u string if len(p.Upstreams) > 0 { u = fmt.Sprintf(", upstreams=%d", len(p.Upstreams)) } return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s%s%s)", p.Type, p.Name, p.Version, u, d) } func removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *distro.Distro) *syftPkg.Collection { byOverlap := map[artifact.ID]artifact.Relationship{} for _, r := range relationships { if r.Type == artifact.OwnershipByFileOverlapRelationship { byOverlap[r.To.ID()] = r } } out := syftPkg.NewCollection() comprehensiveDistroFeed := distroFeedIsComprehensive(distro) for p := range catalog.Enumerate() { r, ok := byOverlap[p.ID()] if ok { from := catalog.Package(r.From.ID()) if from != nil && excludePackage(comprehensiveDistroFeed, p, *from) { continue } } out.Add(p) } return out } func excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syftPkg.Package) bool { // NOTE: we are not checking the name because we have mismatches like: // python 3.9.2 binary // python3.9 3.9.2-1 deb // If the version is not approximately the same, keep both if !strings.HasPrefix(parent.Version, p.Version) && !strings.HasPrefix(p.Version, parent.Version) { return false } // If the parent is an OS package and the child is not, exclude the child // for distros that have a comprehensive feed. That is, distros that list // vulnerabilities that aren't fixed. Otherwise, the child package might // be needed for matching. if comprehensiveDistroFeed && isOSPackage(parent) && !isOSPackage(p) { return true } // filter out binary packages, even for non-comprehensive distros if p.Type != syftPkg.BinaryPkg { return false } return true } // distroFeedIsComprehensive returns true if the distro feed // is comprehensive enough that we can drop packages owned by distro packages // before matching. func distroFeedIsComprehensive(dst *distro.Distro) bool { // TODO: this mechanism should be re-examined once https://github.com/anchore/grype/issues/1426 // is addressed if dst == nil { return false } if dst.Type == distro.AmazonLinux { // AmazonLinux shows "like rhel" but is not an rhel clone // and does not have an exhaustive vulnerability feed. return false } for _, d := range comprehensiveDistros { if strings.EqualFold(string(d), dst.Name()) { return true } for _, n := range dst.IDLike { if strings.EqualFold(string(d), n) { return true } } } return false } // computed by: // sqlite3 vulnerability.db 'select distinct namespace from vulnerability where fix_state in ("wont-fix", "not-fixed") order by namespace;' | cut -d ':' -f 1 | sort | uniq // then removing 'github' var comprehensiveDistros = []distro.Type{ distro.ArchLinux, distro.Azure, distro.Debian, distro.Mariner, distro.RedHat, distro.Ubuntu, } func isOSPackage(p syftPkg.Package) bool { switch p.Type { case syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.PortagePkg, syftPkg.AlpmPkg, syftPkg.ApkPkg: return true default: return false } } func dataFromPkg(p syftPkg.Package) (any, []UpstreamPackage) { var metadata interface{} var upstreams []UpstreamPackage // use the metadata to determine the type of package switch p.Metadata.(type) { case syftPkg.GolangModuleEntry, syftPkg.GolangBinaryBuildinfoEntry, syftPkg.GolangSourceEntry: metadata = golangMetadataFromPkg(p) case syftPkg.DpkgDBEntry: upstreams = dpkgDataFromPkg(p) case syftPkg.DpkgArchiveEntry: upstreams = dpkgDataFromPkg(p) case syftPkg.RpmArchive, syftPkg.RpmDBEntry: m, u := rpmDataFromPkg(p) upstreams = u if m != nil { metadata = *m } case syftPkg.JavaArchive: if m := javaDataFromPkgMetadata(p); m != nil { metadata = *m } case syftPkg.ApkDBEntry: metadata = apkMetadataFromPkg(p) upstreams = apkDataFromPkg(p) case syftPkg.JavaVMInstallation: metadata = javaVMDataFromPkg(p) } // there are still cases where we could still fill the metadata from other info (such as the PURL) if metadata == nil { if p.Type == syftPkg.JavaPkg { metadata = javaDataFromPkgData(p) } } return metadata, upstreams } func javaVMDataFromPkg(p syftPkg.Package) any { if value, ok := p.Metadata.(syftPkg.JavaVMInstallation); ok { return JavaVMInstallationMetadata{ Release: JavaVMReleaseMetadata{ JavaRuntimeVersion: value.Release.JavaRuntimeVersion, JavaVersion: value.Release.JavaVersion, FullVersion: value.Release.FullVersion, SemanticVersion: value.Release.SemanticVersion, }, } } return nil } func apkMetadataFromPkg(p syftPkg.Package) interface{} { if m, ok := p.Metadata.(syftPkg.ApkDBEntry); ok { metadata := ApkMetadata{} fileRecords := make([]ApkFileRecord, 0, len(m.Files)) for _, record := range m.Files { r := ApkFileRecord{Path: record.Path} fileRecords = append(fileRecords, r) } metadata.Files = fileRecords return metadata } return nil } func golangMetadataFromPkg(p syftPkg.Package) interface{} { switch value := p.Metadata.(type) { case syftPkg.GolangBinaryBuildinfoEntry: metadata := GolangBinMetadata{} if value.BuildSettings != nil { metadata.BuildSettings = value.BuildSettings } metadata.GoCompiledVersion = value.GoCompiledVersion metadata.Architecture = value.Architecture metadata.H1Digest = value.H1Digest metadata.MainModule = value.MainModule return metadata case syftPkg.GolangModuleEntry: metadata := GolangModMetadata{} metadata.H1Digest = value.H1Digest return metadata case syftPkg.GolangSourceEntry: metadata := GolangSourceMetadata{} metadata.H1Digest = value.H1Digest metadata.OperatingSystem = value.OperatingSystem metadata.Architecture = value.Architecture metadata.BuildTags = value.BuildTags metadata.CgoEnabled = value.CgoEnabled return metadata } return nil } func dpkgDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) { switch value := p.Metadata.(type) { case syftPkg.DpkgDBEntry: if value.Source != "" { upstreams = append(upstreams, UpstreamPackage{ Name: value.Source, Version: value.SourceVersion, }) } case syftPkg.DpkgArchiveEntry: if value.Source != "" { upstreams = append(upstreams, UpstreamPackage{ Name: value.Source, Version: value.SourceVersion, }) } default: log.Debugf("unable to extract DPKG metadata for %s", p) } return upstreams } func rpmDataFromPkg(p syftPkg.Package) (metadata *RpmMetadata, upstreams []UpstreamPackage) { switch m := p.Metadata.(type) { case syftPkg.RpmDBEntry: if m.SourceRpm != "" { upstreams = handleSourceRPM(p.Name, m.SourceRpm) } metadata = &RpmMetadata{ Epoch: m.Epoch, ModularityLabel: m.ModularityLabel, } case syftPkg.RpmArchive: if m.SourceRpm != "" { upstreams = handleSourceRPM(p.Name, m.SourceRpm) } metadata = &RpmMetadata{ Epoch: m.Epoch, ModularityLabel: m.ModularityLabel, } } return metadata, upstreams } func handleSourceRPM(pkgName, sourceRpm string) []UpstreamPackage { var upstreams []UpstreamPackage name, version := getNameAndELVersion(sourceRpm) if name == "" && version == "" { log.Debugf("unable to extract name and version from SourceRPM=%q", sourceRpm) } else if name != pkgName { // don't include matches if the source package name matches the current package name if name != "" && version != "" { upstreams = append(upstreams, UpstreamPackage{ Name: name, Version: version, }, ) } } return upstreams } func getNameAndELVersion(sourceRpm string) (string, string) { groupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) version := groupMatches["version"] + "-" + groupMatches["release"] return groupMatches["name"], version } func javaDataFromPkgMetadata(p syftPkg.Package) (metadata *JavaMetadata) { if value, ok := p.Metadata.(syftPkg.JavaArchive); ok { var artifactID, groupID, name string if value.PomProperties != nil { artifactID = value.PomProperties.ArtifactID groupID = value.PomProperties.GroupID } else { // get the group ID / artifact ID from the PURL artifactID, groupID = javaGroupArtifactIDFromPurl(p.PURL) } if value.Manifest != nil { for _, kv := range value.Manifest.Main { if kv.Key == "Name" { name = kv.Value } } } var archiveDigests []Digest if len(value.ArchiveDigests) > 0 { for _, d := range value.ArchiveDigests { archiveDigests = append(archiveDigests, Digest{ Algorithm: d.Algorithm, Value: d.Value, }) } } metadata = &JavaMetadata{ VirtualPath: value.VirtualPath, PomArtifactID: artifactID, PomGroupID: groupID, ManifestName: name, ArchiveDigests: archiveDigests, } } return metadata } func javaDataFromPkgData(p syftPkg.Package) (metadata *JavaMetadata) { switch p.Type { case syftPkg.JavaPkg: artifactID, groupID := javaGroupArtifactIDFromPurl(p.PURL) if artifactID != "" && groupID != "" { metadata = &JavaMetadata{ PomArtifactID: artifactID, PomGroupID: groupID, } } default: log.Debugf("unable to extract metadata for %s", p) } return metadata } func javaGroupArtifactIDFromPurl(p string) (string, string) { purl, err := packageurl.FromString(p) if err != nil { log.WithFields("purl", purl, "error", err).Debug("unable to parse java PURL") return "", "" } return purl.Name, purl.Namespace } func apkDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) { if value, ok := p.Metadata.(syftPkg.ApkDBEntry); ok { if value.OriginPackage != "" { upstreams = append(upstreams, UpstreamPackage{ Name: value.OriginPackage, }) } } else { log.Debugf("unable to extract APK metadata for %s", p) } return upstreams } func ByID(id ID, pkgs []Package) *Package { for _, p := range pkgs { if p.ID == id { return &p } } return nil } func parseUpstream(pkgName string, value string, pkgType syftPkg.Type) []UpstreamPackage { if pkgType == syftPkg.RpmPkg { return handleSourceRPM(pkgName, value) } return handleDefaultUpstream(pkgName, value) } func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage { fields := strings.Split(value, "@") switch len(fields) { case 2: if fields[0] == pkgName { return nil } return []UpstreamPackage{ { Name: fields[0], Version: fields[1], }, } case 1: if fields[0] == pkgName { return nil } return []UpstreamPackage{ { Name: fields[0], }, } } return nil } func applyDistroOverride(override *distro.Distro) Enhancer { // the override here comes from either the --distro flag, which already has a channel indication applied return func(out *Package, _ packageurl.PackageURL, _ syftPkg.Package) { if override == nil { return } // allow downstream matchers to always consider the given user distro out.Distro = override } } ================================================ FILE: grype/pkg/package_test.go ================================================ package pkg import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" syftFile "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/testutil" ) func TestNew(t *testing.T) { tests := []struct { name string syftPkg syftPkg.Package metadata interface{} upstreams []UpstreamPackage }{ { name: "alpm package with source info", syftPkg: syftPkg.Package{ Metadata: syftPkg.AlpmDBEntry{ BasePackage: "base-pkg-info", Package: "pkg-info", Version: "version-info", Architecture: "arch-info", Files: []syftPkg.AlpmFileRecord{{ Path: "/this/path/exists", }}, }, }, }, { name: "dpkg with source info", syftPkg: syftPkg.Package{ Metadata: syftPkg.DpkgDBEntry{ Package: "pkg-info", Source: "src-info", Version: "version-info", SourceVersion: "src-version-info", Architecture: "arch-info", Maintainer: "maintainer-info", InstalledSize: 10, Files: []syftPkg.DpkgFileRecord{ { Path: "path-info", Digest: &file.Digest{ Algorithm: "algo-info", Value: "digest-info", }, IsConfigFile: true, }, }, }, }, upstreams: []UpstreamPackage{ { Name: "src-info", Version: "src-version-info", }, }, }, { name: "dpkg archive with source info", syftPkg: syftPkg.Package{ Metadata: syftPkg.DpkgArchiveEntry{ Package: "pkg-info", Source: "src-info", Version: "version-info", SourceVersion: "src-version-info", Architecture: "arch-info", Maintainer: "maintainer-info", InstalledSize: 10, Files: []syftPkg.DpkgFileRecord{ { Path: "path-info", Digest: &file.Digest{ Algorithm: "algo-info", Value: "digest-info", }, IsConfigFile: true, }, }, }, }, upstreams: []UpstreamPackage{ { Name: "src-info", Version: "src-version-info", }, }, }, { name: "rpm archive with source info", syftPkg: syftPkg.Package{ Metadata: syftPkg.RpmArchive{ Name: "name-info", Version: "version-info", Epoch: intRef(30), Arch: "arch-info", Release: "release-info", SourceRpm: "sqlite-3.26.0-6.el8.src.rpm", Size: 40, Vendor: "vendor-info", Files: []syftPkg.RpmFileRecord{ { Path: "path-info", Mode: 20, Size: 10, Digest: file.Digest{ Algorithm: "algo-info", Value: "digest-info", }, UserName: "user-info", GroupName: "group-info", Flags: "flag-info", }, }, }, }, metadata: RpmMetadata{ Epoch: intRef(30), }, upstreams: []UpstreamPackage{ { Name: "sqlite", Version: "3.26.0-6.el8", }, }, }, { name: "rpm db entry with source info", syftPkg: syftPkg.Package{ Metadata: syftPkg.RpmDBEntry{ Name: "name-info", Version: "version-info", Epoch: intRef(30), Arch: "arch-info", Release: "release-info", SourceRpm: "sqlite-3.26.0-6.el8.src.rpm", Size: 40, Vendor: "vendor-info", Files: []syftPkg.RpmFileRecord{ { Path: "path-info", Mode: 20, Size: 10, Digest: file.Digest{ Algorithm: "algo-info", Value: "digest-info", }, UserName: "user-info", GroupName: "group-info", Flags: "flag-info", }, }, }, }, metadata: RpmMetadata{ Epoch: intRef(30), }, upstreams: []UpstreamPackage{ { Name: "sqlite", Version: "3.26.0-6.el8", }, }, }, { name: "rpm archive with source info that matches the package info", syftPkg: syftPkg.Package{ Name: "sqlite", Metadata: syftPkg.RpmArchive{ SourceRpm: "sqlite-3.26.0-6.el8.src.rpm", }, }, metadata: RpmMetadata{}, }, { name: "rpm archive with modularity label", syftPkg: syftPkg.Package{ Name: "sqlite", Metadata: syftPkg.RpmArchive{ SourceRpm: "sqlite-3.26.0-6.el8.src.rpm", ModularityLabel: strRef("abc:2"), }, }, metadata: RpmMetadata{ModularityLabel: strRef("abc:2")}, }, { name: "java pkg", syftPkg: syftPkg.Package{ Metadata: syftPkg.JavaArchive{ VirtualPath: "virtual-path-info", Manifest: &syftPkg.JavaManifest{ Main: syftPkg.KeyValues{ { Key: "Name", Value: "main-section-name-info", }, }, Sections: []syftPkg.KeyValues{ { { Key: "named-section-key", Value: "named-section-value", }, }, }, }, PomProperties: &syftPkg.JavaPomProperties{ Path: "pom-path-info", Name: "pom-name-info", GroupID: "pom-group-ID-info", ArtifactID: "pom-artifact-ID-info", Version: "pom-version-info", Extra: map[string]string{ "extra-key": "extra-value", }, }, ArchiveDigests: []syftFile.Digest{{ Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }}, }, }, metadata: JavaMetadata{ VirtualPath: "virtual-path-info", PomArtifactID: "pom-artifact-ID-info", PomGroupID: "pom-group-ID-info", ManifestName: "main-section-name-info", ArchiveDigests: []Digest{{ Algorithm: "sha1", Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211", }}, }, }, { name: "apk with source info", syftPkg: syftPkg.Package{ Metadata: syftPkg.ApkDBEntry{ Package: "libcurl-tools", OriginPackage: "libcurl", Maintainer: "somone", Version: "1.2.3", Architecture: "a", URL: "a", Description: "a", Size: 1, InstalledSize: 1, }, }, upstreams: []UpstreamPackage{ { Name: "libcurl", }, }, metadata: ApkMetadata{Files: []ApkFileRecord{}}, }, // the below packages are those that have no metadata or upstream info to parse out { name: "npm-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.NpmPackage{ Author: "a", Homepage: "a", Description: "a", URL: "a", }, }, }, { name: "python-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PythonPackage{ Name: "a", Version: "a", Author: "a", AuthorEmail: "a", Platform: "a", SitePackagesRootPath: "a", }, }, }, { name: "gem-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.RubyGemspec{ Name: "a", Version: "a", Homepage: "a", }, }, }, { name: "kb-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.MicrosoftKbPatch{ ProductID: "a", Kb: "a", }, }, }, { name: "rust-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.RustCargoLockEntry{ Name: "a", Version: "a", Source: "a", Checksum: "a", }, }, }, { name: "github-actions-use-statement", syftPkg: syftPkg.Package{ Metadata: syftPkg.GitHubActionsUseStatement{ Value: "a", Comment: "a", }, }, }, { name: "golang-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.GolangBinaryBuildinfoEntry{ BuildSettings: syftPkg.KeyValues{}, GoCompiledVersion: "1.0.0", H1Digest: "a", MainModule: "myMainModule", }, }, metadata: GolangBinMetadata{ BuildSettings: syftPkg.KeyValues{}, GoCompiledVersion: "1.0.0", H1Digest: "a", MainModule: "myMainModule", }, }, { name: "golang-mod-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.GolangModuleEntry{ H1Digest: "h1:as234NweNNTNWEtt13nwNENTt", }, }, metadata: GolangModMetadata{ H1Digest: "h1:as234NweNNTNWEtt13nwNENTt", }, }, { name: "php-composer-lock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PhpComposerLockEntry{ Name: "a", Version: "a", }, }, }, { name: "php-composer-installed-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PhpComposerInstalledEntry{ Name: "a", Version: "a", }, }, }, { name: "dart-publock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.DartPubspecLockEntry{ Name: "a", Version: "a", }, }, }, { name: "dart-pubspec-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.DartPubspec{ Homepage: "a", Repository: "a", Documentation: "a", PublishTo: "a", Environment: &syftPkg.DartPubspecEnvironment{ SDK: "a", Flutter: "a", }, Platforms: []string{"a"}, IgnoredAdvisories: []string{"a"}, }, }, }, { name: "homebrew-formula-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.HomebrewFormula{ Tap: "a", Homepage: "a", Description: "a", }, }, }, { name: "dotnet-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.DotnetDepsEntry{ Name: "a", Version: "a", Path: "a", Sha512: "a", HashPath: "a", }, }, }, { name: "cpp conan-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.ConanfileEntry{ Ref: "catch2/2.13.8", }, }, }, { name: "cpp conan v1 lock metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.ConanV1LockEntry{ Ref: "zlib/1.2.12", Options: syftPkg.KeyValues{ { Key: "fPIC", Value: "True", }, { Key: "shared", Value: "false", }, }, Path: "all/conanfile.py", Context: "host", }, }, }, { name: "cpp conan v2 lock metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.ConanV2LockEntry{ Ref: "zlib/1.2.12", PackageID: "some-id", }, }, }, { name: "cocoapods cocoapods-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.CocoaPodfileLockEntry{ Checksum: "123eere234", }, }, }, { name: "portage-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PortageEntry{ InstalledSize: 1, Files: []syftPkg.PortageFileRecord{}, }, }, }, { name: "hackage-stack-lock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.HackageStackYamlLockEntry{ PkgHash: "some-hash", }, }, }, { name: "hackage-stack-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.HackageStackYamlEntry{ PkgHash: "some-hash", }, }, }, { name: "rebar-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.ErlangRebarLockEntry{ Name: "rebar", Version: "v0.1.1", }, }, }, { name: "npm-package-lock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.NpmPackageLockEntry{ Resolved: "resolved", Integrity: "sha1:ab7d8979989b7a98d97", }, }, }, { name: "mix-lock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.ElixirMixLockEntry{ Name: "mix-lock", Version: "v0.1.2", }, }, }, { name: "pipfile-lock-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PythonPipfileLockEntry{ Hashes: []string{ "sha1:ab8v88a8b88d8d8c88b8s765s47", }, Index: "1", }, }, }, { name: "python-requirements-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PythonRequirementsEntry{ Name: "a", Extras: []string{"a"}, VersionConstraint: "a", URL: "a", Markers: "a", }, }, }, { name: "binary-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.BinarySignature{ Matches: []syftPkg.ClassifierMatch{ { Classifier: "node", }, }, }, }, }, { name: "nix-store-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.NixStoreEntry{ OutputHash: "a", Output: "a", Files: []string{ "a", }, }, }, }, { name: "linux-kernel-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.LinuxKernel{ Name: "a", Architecture: "a", Version: "a", ExtendedVersion: "a", BuildTime: "a", Author: "a", Format: "a", RWRootFS: true, SwapDevice: 10, RootDevice: 11, VideoMode: "a", }, }, }, { name: "linux-kernel-module-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.LinuxKernelModule{ Name: "a", Version: "a", SourceVersion: "a", Path: "a", Description: "a", Author: "a", License: "a", KernelVersion: "a", VersionMagic: "a", Parameters: map[string]syftPkg.LinuxKernelModuleParameter{ "a": { Type: "a", Description: "a", }, }, }, }, }, { name: "r-description-file-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.RDescription{ Title: "a", Description: "a", Author: "a", Maintainer: "a", URL: []string{"a"}, Repository: "a", Built: "a", NeedsCompilation: true, Imports: []string{"a"}, Depends: []string{"a"}, Suggests: []string{"a"}, }, }, }, { name: "dotnet-portable-executable-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.DotnetPortableExecutableEntry{ AssemblyVersion: "a", LegalCopyright: "a", Comments: "a", InternalName: "a", CompanyName: "a", ProductName: "a", ProductVersion: "a", }, }, }, { name: "swift-package-manager-metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.SwiftPackageManagerResolvedEntry{ Revision: "a", }, }, }, { name: "swipl-pack-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.SwiplPackEntry{ Name: "a", Version: "a", Author: "a", AuthorEmail: "a", Packager: "a", PackagerEmail: "a", Homepage: "a", Dependencies: []string{ "a", }, }, }, }, { name: "conaninfo-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.ConaninfoEntry{ Ref: "a", PackageID: "a", }, }, }, { name: "rust-binary-audit-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.RustBinaryAuditEntry{ Name: "a", Version: "a", Source: "a", }, }, }, { name: "python-poetry-lock-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.PythonPoetryLockEntry{Index: "some-index"}, }, }, { name: "yarn-lock-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.YarnLockEntry{ Resolved: "some-resolution", Integrity: "some-digest", }, }, }, { name: "wordpress-plugin-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.WordpressPluginEntry{ PluginInstallDirectory: "a", Author: "a", AuthorURI: "a", }, }, }, { name: "elf-binary-package", syftPkg: syftPkg.Package{ Metadata: syftPkg.ELFBinaryPackageNoteJSONPayload{ Type: "a", Vendor: "a", System: "a", SourceRepo: "a", Commit: "a", }, }, }, { name: "php-pecl-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.PhpPeclEntry{ Name: "a", Version: "a", License: []string{"a"}, }, }, }, { name: "php-pear-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.PhpPearEntry{ Name: "a", Version: "a", }, }, }, { name: "lua-rocks-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.LuaRocksPackage{ Name: "a", Version: "a", License: "a", Homepage: "a", Description: "a", URL: "a", Dependencies: map[string]string{"b": "c"}, }, }, }, { name: "ocaml-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.OpamPackage{ Name: "a", Version: "a", Licenses: []string{"a"}, URL: "a", Checksums: []string{"a"}, Homepage: "a", Dependencies: []string{"a"}, }, }, }, { name: "jvm-installation-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.JavaVMInstallation{ Release: syftPkg.JavaVMRelease{ Implementor: "a", ImplementorVersion: "a", JavaRuntimeVersion: "b", JavaVersion: "c", JavaVersionDate: "a", Libc: "a", Modules: []string{"a"}, OsArch: "a", OsName: "a", OsVersion: "a", Source: "a", BuildSource: "a", BuildSourceRepo: "a", SourceRepo: "a", FullVersion: "d", SemanticVersion: "e", BuildInfo: "a", JvmVariant: "a", JvmVersion: "a", ImageType: "a", BuildType: "a", }, Files: []string{"a"}, }, }, metadata: JavaVMInstallationMetadata{ Release: JavaVMReleaseMetadata{ JavaRuntimeVersion: "b", JavaVersion: "c", FullVersion: "d", SemanticVersion: "e", }, }, }, { name: "dotnet-package-lock-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.DotnetPackagesLockEntry{ Name: "AutoMapper", Version: "13.0.1", ContentHash: "/Fx1SbJ16qS7dU4i604Sle+U9VLX+WSNVJggk6MupKVkYvvBm4XqYaeFuf67diHefHKHs50uQIS2YEDFhPCakQ==", Type: "Direct", }, }, }, { name: "bitnami-sbom-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.BitnamiSBOMEntry{ Name: "a", Version: "1", }, }, }, { name: "terraform-lock-provider-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.TerraformLockProviderEntry{ URL: "registry.terraform.io/hashicorp/aws", Version: "5.72.1", Constraints: "> 5.72.0", Hashes: []string{ "h1:jhd5O5o0CfZCNEwwN0EiDAzb7ApuFrtxJqa6HXW4EKE=", "zh:0dea6843836e926d33469b48b948744079023816d16a2ff7666bcfb6aa3522d4", "zh:195fa9513f75800a0d62797ebec75ee73e9b8c28d713fe9b63d3b1d1eec129b3", }, }, }, }, { name: "pe binary metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PEBinary{ VersionResources: syftPkg.KeyValues{ { Key: "k", Value: "k", }, }, }, }, }, { name: "uv lock metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.PythonUvLockEntry{ Index: "https://pypi.org/simple", Dependencies: []syftPkg.PythonUvLockDependencyEntry{ {Name: "certifi"}, {Name: "charset-normalizer"}, {Name: "idna"}, {Name: "urllib3"}, }, }, }, }, { name: "conda meta package metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.CondaMetaPackage{ Name: "numpy", Version: "1.21.0", Build: "py39h20b1b1c_0", BuildNumber: 0, Channel: "conda-forge", }, }, }, { name: "golang source entry metadata", syftPkg: syftPkg.Package{ Metadata: syftPkg.GolangSourceEntry{ H1Digest: "h1:some-hash-value", OperatingSystem: "linux", Architecture: "amd64", BuildTags: "release", CgoEnabled: true, }, }, metadata: GolangSourceMetadata{ H1Digest: "h1:some-hash-value", OperatingSystem: "linux", Architecture: "amd64", BuildTags: "release", CgoEnabled: true, }, }, { name: "snap-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.SnapEntry{ SnapType: "app", Base: "core22", SnapName: "test-snap", SnapVersion: "1.0.0", Architecture: "amd64", }, }, }, { name: "python-pdm-lock-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.PythonPdmLockEntry{ Summary: "Test package", Files: []syftPkg.PythonPdmFileEntry{ { URL: "test/file.py", }, }, Dependencies: []string{"dependency1", "dependency2"}, }, }, }, { name: "javascript-pnpm-lock-entry", syftPkg: syftPkg.Package{ Metadata: syftPkg.PnpmLockEntry{ Resolution: syftPkg.PnpmLockResolution{ Integrity: "", }, Dependencies: map[string]string{ "dependency1": "1.2.3", "dependency2": "4.5.6"}, }, }, }, { name: "gguf-file-header", syftPkg: syftPkg.Package{ Metadata: syftPkg.GGUFFileHeader{ GGUFVersion: 1, FileSize: 2, Architecture: "arch", Quantization: "quant", Parameters: 3, TensorCount: 4, RemainingKeyValues: map[string]any{ "key1": "value1", }, MetadataKeyValuesHash: "f00bar123", }, }, }, } // capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing tester := testutil.NewPackageMetadataCompletionTester(t) // run all of our cases for _, test := range tests { t.Run(test.name, func(t *testing.T) { tester.Tested(t, test.syftPkg.Metadata) p := New(test.syftPkg) assert.Equal(t, test.metadata, p.Metadata, "unexpected metadata") assert.Equal(t, test.upstreams, p.Upstreams, "unexpected upstream") }) } } func TestFromCollection_DoesNotPanic(t *testing.T) { collection := syftPkg.NewCollection() examplePackage := syftPkg.Package{ Name: "test", Version: "1.2.3", Locations: file.NewLocationSet( file.NewLocation("/test-path"), ), Type: syftPkg.NpmPkg, } collection.Add(examplePackage) // add it again! collection.Add(examplePackage) assert.NotPanics(t, func() { _ = FromCollection(collection, SynthesisConfig{}) }) } func TestFromCollection_GeneratesCPEs(t *testing.T) { collection := syftPkg.NewCollection() collection.Add(syftPkg.Package{ Name: "first", Version: "1", CPEs: []cpe.CPE{ {}, }, }) collection.Add(syftPkg.Package{ Name: "second", Version: "2", }) // doesn't generate cpes when no flag pkgs := FromCollection(collection, SynthesisConfig{}) assert.Len(t, pkgs[0].CPEs, 1) assert.Len(t, pkgs[1].CPEs, 0) // does generate cpes with the flag pkgs = FromCollection(collection, SynthesisConfig{ GenerateMissingCPEs: true, }) assert.Len(t, pkgs[0].CPEs, 1) assert.Len(t, pkgs[1].CPEs, 1) } func Test_getNameAndELVersion(t *testing.T) { tests := []struct { name string sourceRPM string expectedName string expectedVersion string }{ { name: "sqlite-3.26.0-6.el8.src.rpm", sourceRPM: "sqlite-3.26.0-6.el8.src.rpm", expectedName: "sqlite", expectedVersion: "3.26.0-6.el8", }, { name: "util-linux-ng-2.17.2-12.28.el6_9.src.rpm", sourceRPM: "util-linux-ng-2.17.2-12.28.el6_9.src.rpm", expectedName: "util-linux-ng", expectedVersion: "2.17.2-12.28.el6_9", }, { name: "util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm", sourceRPM: "util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm", expectedName: "util-linux-ng", expectedVersion: "2.17.2-12.28.el6_9.2", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actualName, actualVersion := getNameAndELVersion(test.sourceRPM) assert.Equal(t, test.expectedName, actualName) assert.Equal(t, test.expectedVersion, actualVersion) }) } } func intRef(i int) *int { return &i } func Test_RemovePackagesByOverlap(t *testing.T) { tests := []struct { name string sbom *sbom.SBOM expectedPackages []string }{ { name: "includes all packages without overlap", sbom: catalogWithOverlaps( []string{":go@1.18", "apk:node@19.2-r1", "binary:python@3.9"}, []string{}), expectedPackages: []string{":go@1.18", "apk:node@19.2-r1", "binary:python@3.9"}, }, { name: "excludes single package by overlap", sbom: catalogWithOverlaps( []string{"apk:go@1.18", "apk:node@19.2-r1", "binary:node@19.2"}, []string{"apk:node@19.2-r1 -> binary:node@19.2"}), expectedPackages: []string{"apk:go@1.18", "apk:node@19.2-r1"}, }, { name: "does not exclude if OS package owns OS package", sbom: catalogWithOverlaps( []string{"rpm:perl@5.3-r1", "rpm:libperl@5.3"}, []string{"rpm:perl@5.3-r1 -> rpm:libperl@5.3"}), expectedPackages: []string{"rpm:libperl@5.3", "rpm:perl@5.3-r1"}, }, { name: "does not exclude if owning package is non-OS", sbom: catalogWithOverlaps( []string{"python:urllib3@1.2.3", "python:otherlib@1.2.3"}, []string{"python:urllib3@1.2.3 -> python:otherlib@1.2.3"}), expectedPackages: []string{"python:otherlib@1.2.3", "python:urllib3@1.2.3"}, }, { name: "excludes multiple package by overlap", sbom: catalogWithOverlaps( []string{"apk:go@1.18", "apk:node@19.2-r1", "binary:node@19.2", "apk:python@3.9-r9", "binary:python@3.9"}, []string{"apk:node@19.2-r1 -> binary:node@19.2", "apk:python@3.9-r9 -> binary:python@3.9"}), expectedPackages: []string{"apk:go@1.18", "apk:node@19.2-r1", "apk:python@3.9-r9"}, }, { name: "does not exclude with different types", sbom: catalogWithOverlaps( []string{"rpm:node@19.2-r1", "apk:node@19.2"}, []string{"rpm:node@19.2-r1 -> apk:node@19.2"}), expectedPackages: []string{"apk:node@19.2", "rpm:node@19.2-r1"}, }, { name: "does not exclude if OS package owns OS package", sbom: catalogWithOverlaps( []string{"rpm:perl@5.3-r1", "rpm:libperl@5.3"}, []string{"rpm:perl@5.3-r1 -> rpm:libperl@5.3"}), expectedPackages: []string{"rpm:libperl@5.3", "rpm:perl@5.3-r1"}, }, { name: "does not exclude if owning package is non-OS", sbom: catalogWithOverlaps( []string{"python:urllib3@1.2.3", "python:otherlib@1.2.3"}, []string{"python:urllib3@1.2.3 -> python:otherlib@1.2.3"}), expectedPackages: []string{"python:otherlib@1.2.3", "python:urllib3@1.2.3"}, }, { name: "python bindings for system RPM install", sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "rhel"), expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8"}, }, { name: "amzn linux doesn't remove packages in this way", sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, []string{"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3"}), "amzn"), expectedPackages: []string{"rpm:python3-rpm@4.14.3-26.el8", "python:rpm@4.14.3"}, }, { name: "remove overlapping package when parent version is prefix of child version", sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt"}, []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt"}), "rhel"), expectedPackages: []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5"}, }, { name: "remove overlapping package when child version is prefix of parent version", sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt", "linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5"}, []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5"}), "rhel"), expectedPackages: []string{"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt"}, }, { name: "do not remove overlapping package when versions are not similar", sbom: withLinuxRelease(catalogWithOverlaps( []string{"rpm:kernel@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@6.17"}, []string{"rpm:kernel@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@6.17"}), "rhel"), expectedPackages: []string{"rpm:kernel@5.14.0-503.40.1.el9_5", "linux-kernel:linux-kernel@6.17"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { d := distro.FromRelease(test.sbom.Artifacts.LinuxDistribution, distro.DefaultFixChannels()) catalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, d) pkgs := FromCollection(catalog, SynthesisConfig{}) var pkgNames []string for _, p := range pkgs { pkgNames = append(pkgNames, fmt.Sprintf("%s:%s@%s", p.Type, p.Name, p.Version)) } assert.EqualValues(t, test.expectedPackages, pkgNames) }) } } func catalogWithOverlaps(packages []string, overlaps []string) *sbom.SBOM { var pkgs []syftPkg.Package var relationships []artifact.Relationship toPkg := func(str string) syftPkg.Package { var typ, name, version string s := strings.Split(strings.TrimSpace(str), ":") if len(s) > 1 { typ = s[0] str = s[1] } s = strings.Split(str, "@") name = s[0] if len(s) > 1 { version = s[1] } p := syftPkg.Package{ Type: syftPkg.Type(typ), Name: name, Version: version, } p.SetID() return p } for _, pkg := range packages { p := toPkg(pkg) pkgs = append(pkgs, p) } for i, overlap := range overlaps { parts := strings.Split(overlap, "->") if len(parts) < 2 { panic("invalid overlap, use -> to specify, e.g.: pkg1->pkg2") } from := toPkg(parts[0]) to := toPkg(parts[1]) // The catalog will type check whether To or From is a pkg.Package or a *pkg.Package. // Previously, there was a bug where Grype assumed that From was always a pkg.Package. // Therefore, intentionally mix pointer and non-pointer packages to prevent Grype from // assuming which is which again. (The correct usage, calling catalog.Package, always // returns a *pkg.Package, and doesn't rely on any type assertion.) if i%2 == 0 { relationships = append(relationships, artifact.Relationship{ From: &from, To: &to, Type: artifact.OwnershipByFileOverlapRelationship, }) } else { relationships = append(relationships, artifact.Relationship{ From: from, To: to, Type: artifact.OwnershipByFileOverlapRelationship, }) } } catalog := syftPkg.NewCollection(pkgs...) return &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: catalog, }, Relationships: relationships, } } func withLinuxRelease(s *sbom.SBOM, id string) *sbom.SBOM { s.Artifacts.LinuxDistribution = &linux.Release{ ID: id, } return s } func strRef(s string) *string { return &s } ================================================ FILE: grype/pkg/provider.go ================================================ package pkg import ( "errors" "fmt" "strings" "github.com/bmatcuk/doublestar/v2" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/sbom" ) var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given source") // Provide a set of packages and context metadata describing where they were sourced from. func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { applyChannel := getDistroChannelApplier(config.Distro.FixChannels) if config.Distro.Override != nil { applyChannel(config.Distro.Override) log.Infof("using distro: %s", config.Distro.Override.String()) } packages, ctx, s, err := provide(userInput, config, applyChannel) if err != nil { return nil, Context{}, nil, err } setContextDistro(packages, &ctx) // set the distro on each package if there is not already one set if ctx.Distro != nil { for i := range packages { if packages[i].Distro == nil { packages[i].Distro = ctx.Distro } } if config.Distro.Override == nil { log.Infof("using distro: %s", ctx.Distro.String()) } } return packages, ctx, s, nil } // buildChannelIndex creates a map of distro IDs to their applicable fix channels func buildChannelIndex(channels []distro.FixChannel) map[string]distro.FixChannels { idx := make(map[string]distro.FixChannels, len(channels)) for _, c := range channels { if c.Name == "" { continue } for _, id := range c.IDs { if id == "" { continue } id = strings.ToLower(id) idx[id] = append(idx[id], c) } } return idx } func getDistroChannelApplier(channels []distro.FixChannel) func(d *distro.Distro) bool { idx := buildChannelIndex(channels) return func(d *distro.Distro) bool { if d == nil { return false } id := strings.ToLower(d.ID()) channels, ok := idx[id] if !ok { return false } return applyChannelsToDistro(d, channels) } } // applyChannelsToDistro applies fix channels to a distro based on channel configuration func applyChannelsToDistro(d *distro.Distro, channels distro.FixChannels) bool { var result []string existing := strset.New(d.Channels...) ver := version.New(d.Version, version.SemanticFormat) shouldReview := func(channel distro.FixChannel) bool { if channel.Versions != nil && ver != nil { isApplicable, err := channel.Versions.Satisfied(ver) if err != nil { log.WithFields("error", err, "constraint", channel.Versions).Debugf("unable to determine if channel %q is applicable for distro %q with version %q", channel.Name, d.Type, ver) return true } return isApplicable } return true } var modified bool for _, channel := range channels { if channel.Name == "" { continue } if !shouldReview(channel) { log.WithFields("channel", channel.Name, "distro", d.Type, "version", ver).Debugf("skipping channel %q for distro %q with version %q", channel.Name, d.Type, ver) continue } switch channel.Apply { case distro.ChannelNeverEnabled: if existing.Has(channel.Name) { modified = true } case distro.ChannelAlwaysEnabled: result = append(result, channel.Name) if !existing.Has(channel.Name) { modified = true } case distro.ChannelConditionallyEnabled: if existing.Has(channel.Name) { result = append(result, channel.Name) } } } d.Channels = result return modified } // Provide a set of packages and context metadata describing where they were sourced from. func provide(userInput string, config ProviderConfig, applyChannel func(d *distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) { packages, ctx, s, err := purlProvider(userInput, config, applyChannel) if !errors.Is(err, errDoesNotProvide) { log.WithFields("input", userInput).Trace("interpreting input as one or more PURLs") return packages, ctx, s, err } packages, ctx, s, err = cpeProvider(userInput, config) if !errors.Is(err, errDoesNotProvide) { log.WithFields("input", userInput).Trace("interpreting input as a one or more CPEs") return packages, ctx, s, err } packages, ctx, s, err = syftSBOMProvider(userInput, config, applyChannel) if !errors.Is(err, errDoesNotProvide) { if len(config.Exclusions) > 0 { var exclusionsErr error packages, exclusionsErr = filterPackageExclusions(packages, config.Exclusions) if exclusionsErr != nil { return nil, ctx, s, exclusionsErr } } log.WithFields("input", userInput).Trace("interpreting input as an SBOM document") return packages, ctx, s, err } log.WithFields("input", userInput).Trace("passing input to syft for interpretation") return syftProvider(userInput, config, applyChannel) } // This will filter the provided packages list based on a set of exclusion expressions. Globs // are allowed for the exclusions. A package will be *excluded* only if *all locations* match // one of the provided exclusions. func filterPackageExclusions(packages []Package, exclusions []string) ([]Package, error) { var out []Package for _, pkg := range packages { includePackage := true locations := pkg.Locations.ToSlice() if len(locations) > 0 { includePackage = false // require ALL locations to be excluded for the package to be excluded location: for _, location := range locations { for _, exclusion := range exclusions { match, err := locationMatches(location, exclusion) if err != nil { return nil, err } if match { continue location } } // if this point is reached, one location has not matched any exclusion, include the package includePackage = true break } } if includePackage { out = append(out, pkg) } } return out, nil } // Test a location RealPath and VirtualPath for a match against the exclusion parameter. // The exclusion allows glob expressions such as `/usr/**` or `**/*.json`. If the exclusion // is an invalid pattern, an error is returned; otherwise, the resulting boolean indicates a match. func locationMatches(location file.Location, exclusion string) (bool, error) { matchesRealPath, err := doublestar.Match(exclusion, location.RealPath) if err != nil { return false, err } matchesVirtualPath, err := doublestar.Match(exclusion, location.AccessPath) if err != nil { return false, err } return matchesRealPath || matchesVirtualPath, nil } func setContextDistro(packages []Package, ctx *Context) { if ctx.Distro != nil { return } var singleDistro *distro.Distro for _, p := range packages { if p.Distro == nil { continue } if singleDistro == nil { singleDistro = p.Distro continue } // if we have a distro already, ensure that the new one matches... if singleDistro.Type != p.Distro.Type || singleDistro.Version != p.Distro.Version || singleDistro.Codename != p.Distro.Codename { // ...if not then we bail, not setting a singular distro in the context return } } // if there is one distro (with one version) represented, use that if singleDistro != nil { ctx.Distro = singleDistro } } ================================================ FILE: grype/pkg/provider_config.go ================================================ package pkg import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" ) type ProviderConfig struct { SyftProviderConfig SynthesisConfig } type SyftProviderConfig struct { SBOMOptions *syft.CreateSBOMConfig RegistryOptions *image.RegistryOptions Platform string Exclusions []string Name string DefaultImagePullSource string Sources []string } type SynthesisConfig struct { GenerateMissingCPEs bool Distro DistroConfig } type DistroConfig struct { Override *distro.Distro FixChannels []distro.FixChannel } ================================================ FILE: grype/pkg/provider_test.go ================================================ package pkg import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/version" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/file" ) func TestProviderLocationExcludes(t *testing.T) { tests := []struct { name string fixture string excludes []string expected []string wantErr assert.ErrorAssertionFunc }{ { name: "exclude everything", fixture: "testdata/syft-spring.json", excludes: []string{"**"}, expected: []string{}, }, { name: "exclude specific real path match", fixture: "testdata/syft-spring.json", excludes: []string{"**/tomcat*.jar"}, expected: []string{"charsets"}, }, { name: "include everything with no match", fixture: "testdata/syft-spring.json", excludes: []string{"**/asdf*.jar"}, expected: []string{"charsets", "tomcat-embed-el"}, }, { name: "include everything with no excludes", fixture: "testdata/syft-spring.json", excludes: []string{}, expected: []string{"charsets", "tomcat-embed-el"}, }, { name: "exclusions must not hide parsing error", fixture: "testdata/bad-sbom.json", excludes: []string{"**/some-glob/*"}, wantErr: assert.Error, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sbomConfig := syft.DefaultCreateSBOMConfig(). WithCatalogerSelection(cataloging.NewSelectionRequest(). WithRemovals("rpm-db-cataloger")) cfg := ProviderConfig{ SyftProviderConfig: SyftProviderConfig{ Exclusions: test.excludes, SBOMOptions: sbomConfig, }, } if test.wantErr == nil { test.wantErr = assert.NoError } pkgs, _, _, err := Provide(test.fixture, cfg) test.wantErr(t, err) if err != nil { return } var pkgNames []string for _, pkg := range pkgs { pkgNames = append(pkgNames, pkg.Name) } assert.ElementsMatch(t, pkgNames, test.expected) }) } } func TestSyftLocationExcludes(t *testing.T) { tests := []struct { name string fixture string excludes []string expected []string }{ { name: "exclude everything", fixture: "image-simple", excludes: []string{"**"}, expected: []string{}, }, { name: "exclude specific real path match", fixture: "image-simple", excludes: []string{"**/nested/package.json"}, expected: []string{"top-level-package"}, }, { name: "include everything with no match", fixture: "image-simple", excludes: []string{"**/asdf*.json"}, expected: []string{"nested-package", "top-level-package"}, }, { name: "include everything with no excludes", fixture: "image-simple", excludes: []string{}, expected: []string{"nested-package", "top-level-package"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { userInput := imagetest.GetFixtureImageTarPath(t, test.fixture) sbomConfig := syft.DefaultCreateSBOMConfig(). WithCatalogerSelection(cataloging.NewSelectionRequest(). WithRemovals("rpm-db-cataloger")) cfg := ProviderConfig{ SyftProviderConfig: SyftProviderConfig{ Exclusions: test.excludes, SBOMOptions: sbomConfig, }, } pkgs, _, _, err := Provide(userInput, cfg) assert.NoErrorf(t, err, "error calling Provide function") var pkgNames []string for _, pkg := range pkgs { pkgNames = append(pkgNames, pkg.Name) } assert.ElementsMatch(t, pkgNames, test.expected) }) } } func Test_filterPackageExclusions(t *testing.T) { tests := []struct { name string locations [][]string exclusions []string expected int }{ { name: "exclude nothing", locations: [][]string{{"/foo", "/bar"}, {"/foo", "/bar"}}, exclusions: []string{"/asdf/**"}, expected: 2, }, { name: "exclude everything", locations: [][]string{{"/foo", "/bar"}, {"/foo", "/bar"}}, exclusions: []string{"**"}, expected: 0, }, { name: "exclude based on all location match", locations: [][]string{{"/foo1", "/bar1"}, {"/foo2", "/bar2"}}, exclusions: []string{"/foo2", "/bar2"}, expected: 1, }, { name: "don't exclude with single location match", locations: [][]string{{"/foo1", "/bar1"}, {"/foo2", "/bar2"}}, exclusions: []string{"/foo1", "/foo2"}, expected: 2, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var packages []Package for _, pkg := range test.locations { locations := file.NewLocationSet() for _, l := range pkg { locations.Add( file.NewVirtualLocation(l, l), ) } packages = append(packages, Package{Locations: locations}) } filtered, err := filterPackageExclusions(packages, test.exclusions) assert.NoError(t, err) assert.Len(t, filtered, test.expected) }) } } func Test_matchesLocation(t *testing.T) { tests := []struct { name string realPath string virtualPath string match string expected bool }{ { name: "doesn't match real", realPath: "/asdf", virtualPath: "", match: "/usr", expected: false, }, { name: "doesn't match virtual", realPath: "", virtualPath: "/asdf", match: "/usr", expected: false, }, { name: "does match real", realPath: "/usr/foo", virtualPath: "", match: "/usr/**", expected: true, }, { name: "does match virtual", realPath: "", virtualPath: "/usr/bar/oof.txt", match: "/usr/**", expected: true, }, { name: "does match file", realPath: "", virtualPath: "/usr/bar/oof.txt", match: "**/*.txt", expected: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { matches, err := locationMatches(file.NewVirtualLocation(test.realPath, test.virtualPath), test.match) assert.NoError(t, err) assert.Equal(t, test.expected, matches) }) } } func Test_getDistroChannelApplier(t *testing.T) { defaultOSGen := func() *distro.Distro { return distro.NewFromNameVersion("rhel", "8.4") } tests := []struct { name string channels []distro.FixChannel distro func() *distro.Distro want []string }{ { name: "nil distro", channels: distro.DefaultFixChannels(), distro: func() *distro.Distro { return nil }, want: nil, }, { name: "no matching channel", channels: distro.DefaultFixChannels(), distro: func() *distro.Distro { return distro.NewFromNameVersion("ubuntu", "20.04") }, want: nil, }, { name: "channel never enabled", channels: []distro.FixChannel{ { Name: "test-channel", IDs: []string{"rhel"}, Apply: distro.ChannelNeverEnabled, }, }, distro: defaultOSGen, want: nil, }, { name: "channel always enabled", channels: []distro.FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: distro.ChannelAlwaysEnabled, }, }, distro: defaultOSGen, want: []string{"eus"}, }, { name: "case insensitive matching", channels: []distro.FixChannel{ { Name: "eus", IDs: []string{"RHEL"}, Apply: distro.ChannelAlwaysEnabled, }, }, distro: defaultOSGen, want: []string{"eus"}, }, { name: "multiple IDs in channel", channels: []distro.FixChannel{ { Name: "test-channel", IDs: []string{"centos", "rhel", "fedora"}, Apply: distro.ChannelAlwaysEnabled, }, }, distro: defaultOSGen, want: []string{"test-channel"}, }, { name: "empty channel name skipped", channels: []distro.FixChannel{ { Name: "", IDs: []string{"rhel"}, Apply: distro.ChannelAlwaysEnabled, }, }, distro: defaultOSGen, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { applier := getDistroChannelApplier(tt.channels) d := tt.distro() applier(d) if d != nil { assert.Equal(t, tt.want, d.Channels) } }) } } func Test_applyChannelsToDistro(t *testing.T) { tests := []struct { name string distro func() *distro.Distro channels distro.FixChannels expectedResult []string expectedModified bool }{ { name: "always enabled channel adds new channel", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "8.4") }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelAlwaysEnabled, }, }, expectedResult: []string{"eus"}, expectedModified: true, }, { name: "always enabled channel keeps existing channel", distro: func() *distro.Distro { d := distro.NewFromNameVersion("rhel", "8.4") d.Channels = []string{"eus"} return d }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelAlwaysEnabled, }, }, expectedResult: []string{"eus"}, expectedModified: false, }, { name: "conditionally enabled channel keeps existing channel", distro: func() *distro.Distro { d := distro.NewFromNameVersion("rhel", "8.4") d.Channels = []string{"eus"} return d }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelConditionallyEnabled, }, }, expectedResult: []string{"eus"}, expectedModified: false, }, { name: "conditionally enabled channel does not add missing channel", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "8.4") }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelConditionallyEnabled, }, }, expectedResult: []string{}, expectedModified: false, }, { name: "never enabled channel removes existing channel", distro: func() *distro.Distro { d := distro.NewFromNameVersion("rhel", "8.4") d.Channels = []string{"eus"} return d }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelNeverEnabled, }, }, expectedResult: []string{}, expectedModified: true, }, { name: "never enabled channel with no existing channel", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "8.4") }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelNeverEnabled, }, }, expectedResult: []string{}, expectedModified: false, }, { name: "empty channel name is skipped", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "8.4") }, channels: distro.FixChannels{ { Name: "", Apply: distro.ChannelAlwaysEnabled, }, { Name: "eus", Apply: distro.ChannelAlwaysEnabled, }, }, expectedResult: []string{"eus"}, expectedModified: true, }, { name: "version constraint allows channel", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "8.4") }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelAlwaysEnabled, Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat), }, }, expectedResult: []string{"eus"}, expectedModified: true, }, { name: "version constraint blocks channel", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "7.9") }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelAlwaysEnabled, Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat), }, }, expectedResult: []string{}, expectedModified: false, }, { name: "multiple channels with different behaviors", distro: func() *distro.Distro { d := distro.NewFromNameVersion("rhel", "8.4") d.Channels = []string{"eus", "optional"} return d }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelConditionallyEnabled, }, { Name: "main", Apply: distro.ChannelAlwaysEnabled, }, { Name: "optional", Apply: distro.ChannelNeverEnabled, }, }, expectedResult: []string{"eus", "main"}, expectedModified: true, }, { name: "invalid version string defaults to allowing channel", distro: func() *distro.Distro { return distro.NewFromNameVersion("rhel", "invalid-version") }, channels: distro.FixChannels{ { Name: "eus", Apply: distro.ChannelAlwaysEnabled, Versions: version.MustGetConstraint(">= 8.0", version.SemanticFormat), }, }, expectedResult: []string{"eus"}, expectedModified: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := tt.distro() modified := applyChannelsToDistro(d, tt.channels) if d := cmp.Diff(tt.expectedResult, d.Channels, cmpopts.EquateEmpty()); d != "" { t.Errorf("applyChannelsToDistro() mismatch (-want +got):\n%s", d) } assert.Equal(t, tt.expectedModified, modified) }) } } ================================================ FILE: grype/pkg/purl_provider.go ================================================ package pkg import ( "fmt" "io" "slices" "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/format" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) const ( purlInputPrefix = "purl:" singlePurlInputPrefix = "pkg:" ) type PURLLiteralMetadata struct { PURL string } func purlEnhancers(applyChannel func(*distro.Distro) bool) []Enhancer { return []Enhancer{setUpstreamsFromPURL, setDistroFromPURL(applyChannel)} } func purlProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) { reader, ctx, err := getPurlReader(userInput) if err != nil { return nil, Context{}, nil, err } s, _, _, err := format.Decode(reader) if s == nil { return nil, Context{}, nil, fmt.Errorf("unable to decode purl: %w", err) } return FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers(applyChannel)...), ctx, s, nil } func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) { if strings.HasPrefix(userInput, singlePurlInputPrefix) { ctx.Source = &source.Description{ Metadata: PURLLiteralMetadata{ PURL: userInput, }, } return strings.NewReader(userInput), ctx, nil } return nil, ctx, errDoesNotProvide } func setUpstreamsFromPURL(out *Package, purl packageurl.PackageURL, syftPkg syftPkg.Package) { if len(out.Upstreams) == 0 || out.PURL == "" { out.Upstreams = upstreamsFromPURL(purl, syftPkg.Type) } } // upstreamsFromPURL reads any additional data Grype can use, which is ignored by Syft's PURL conversion func upstreamsFromPURL(purl packageurl.PackageURL, pkgType syftPkg.Type) (upstreams []UpstreamPackage) { for _, qualifier := range purl.Qualifiers { if qualifier.Key == syftPkg.PURLQualifierUpstream { for _, newUpstream := range parseUpstream(purl.Name, qualifier.Value, pkgType) { if slices.Contains(upstreams, newUpstream) { continue } upstreams = append(upstreams, newUpstream) } } } return upstreams } func setDistroFromPURL(applyChannel func(*distro.Distro) bool) func(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) { return func(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) { if out.Distro == nil { out.Distro = distroFromPURL(purl) applyChannel(out.Distro) } } } // distroFromPURL reads distro data for Grype can use, which is ignored by Syft's PURL conversion func distroFromPURL(purl packageurl.PackageURL) (d *distro.Distro) { var distroName, distroVersion string for _, qualifier := range purl.Qualifiers { if qualifier.Key == syftPkg.PURLQualifierDistro { // Use shared parsing logic to support -, :, and @ separators distroName, distroVersion = distro.ParseDistroString(qualifier.Value) } } if distroName != "" { d = distro.NewFromNameVersion(distroName, distroVersion) } return d } ================================================ FILE: grype/pkg/purl_provider_test.go ================================================ package pkg import ( "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/version" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) var diffOpts = []cmp.Option{ cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Language", "CPEs"), cmpopts.IgnoreUnexported(distro.Distro{}), } func Test_PurlProvider(t *testing.T) { tests := []struct { name string userInput string channels []distro.FixChannel wantContext Context wantPkgs []Package wantErr require.ErrorAssertionFunc }{ { name: "takes a single purl", userInput: "pkg:apk/curl@7.61.1", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:apk/curl@7.61.1", }, }, }, wantPkgs: []Package{ { Name: "curl", Version: "7.61.1", Type: pkg.ApkPkg, PURL: "pkg:apk/curl@7.61.1", }, }, }, { name: "java metadata decoded from purl", userInput: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", }, }, }, wantPkgs: []Package{ { Name: "commons-lang3", Version: "3.12.0", Type: pkg.JavaPkg, PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", Metadata: JavaMetadata{ PomArtifactID: "commons-lang3", PomGroupID: "org.apache.commons", }, }, }, }, { name: "os with codename", userInput: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", }, }, }, wantPkgs: []Package{ { Name: "sysv-rc", Version: "2.88dsf-59", Type: pkg.DebPkg, PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit", Distro: &distro.Distro{Type: distro.Debian, Version: "", Codename: "jessie", IDLike: []string{"debian"}}, Upstreams: []UpstreamPackage{ { Name: "sysvinit", }, }, }, }, }, { name: "default upstream", userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", }, }, }, wantPkgs: []Package{ { Name: "libcrypto3", Version: "3.3.2", Type: pkg.ApkPkg, PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", Upstreams: []UpstreamPackage{ { Name: "openssl", }, }, }, }, }, { name: "upstream with version", userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @ channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", }, }, }, wantPkgs: []Package{ { Name: "libcrypto3", Version: "3.3.2", Type: pkg.ApkPkg, PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", Upstreams: []UpstreamPackage{ { Name: "openssl", Version: "3.2.1", }, }, }, }, }, { name: "upstream for source RPM", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", }, }, }, wantPkgs: []Package{ { Name: "systemd-x", Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}}, Upstreams: []UpstreamPackage{ { Name: "systemd", Version: "239-82.el8_10.2", }, }, }, }, }, { name: "RPM with epoch", userInput: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", }, }, }, wantPkgs: []Package{ { Name: "dbus-common", Version: "1:1.12.8-26.el8", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Codename: "", IDLike: []string{"redhat"}}, Upstreams: []UpstreamPackage{ { Name: "dbus", Version: "1.12.8-26.el8", }, }, }, }, }, { name: "RPM with rpmmod", userInput: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4", }, }, }, wantPkgs: []Package{ { Name: "httpd", Version: "2.4.37-51", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4", Distro: &distro.Distro{Type: distro.RedHat, Version: "8.7", Codename: "", IDLike: []string{"redhat"}}, Metadata: RpmMetadata{ ModularityLabel: strRef("httpd:2.4"), }, }, }, }, { name: "infer context when distro is present for single purl", userInput: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", }, }, }, wantPkgs: []Package{ { Name: "curl", Version: "7.61.1", Type: pkg.ApkPkg, PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", Codename: "", IDLike: []string{"alpine"}}, }, }, }, { name: "include namespace in name when purl is type Golang", userInput: "pkg:golang/k8s.io/ingress-nginx@v1.11.2", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2"}, }, }, wantPkgs: []Package{ { Name: "k8s.io/ingress-nginx", Version: "v1.11.2", Type: pkg.GoModulePkg, PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2", }, }, }, { name: "include complex namespace in name when purl is type Golang", userInput: "pkg:golang/github.com/wazuh/wazuh@v4.5.0", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0"}, }, }, wantPkgs: []Package{ { Name: "github.com/wazuh/wazuh", Version: "v4.5.0", Type: pkg.GoModulePkg, PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0", }, }, }, { name: "do not include namespace when given blank input blank", userInput: "pkg:golang/wazuh@v4.5.0", channels: testFixChannels(), wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{PURL: "pkg:golang/wazuh@v4.5.0"}, }, }, wantPkgs: []Package{ { Name: "wazuh", Version: "v4.5.0", Type: pkg.GoModulePkg, PURL: "pkg:golang/wazuh@v4.5.0", }, }, }, { name: "RPM with extended support (auto)", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", channels: testFixChannels(), // important! auto applies EUS wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", }, }, }, wantPkgs: []Package{ { Name: "systemd-x", Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Channels: names("eus"), IDLike: []string{"redhat"}}, }, }, }, { name: "RPM with extended support (never)", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", channels: []distro.FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: distro.ChannelNeverEnabled, // important! }, }, wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", // the input did hint hat eus, so we leave it }, }, }, wantPkgs: []Package{ { Name: "systemd-x", Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus", // important! we are NOT patching the channel out of the PURL Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Channels: nil, IDLike: []string{"redhat"}}, // important! no channel applied }, }, }, { name: "RPM without extended support (always)", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", // important! no channel hint channels: []distro.FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: distro.ChannelAlwaysEnabled, // important! }, }, wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", }, }, }, wantPkgs: []Package{ { Name: "systemd-x", Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", // important! we are NOT patching the channel into the PURL Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", Channels: names("eus"), IDLike: []string{"redhat"}}, // important! channel applied }, }, }, { name: "RPM without extended support (always) outside of version range", userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", // important! no channel hint channels: []distro.FixChannel{ { Name: "eus", IDs: []string{"rhel"}, Apply: distro.ChannelAlwaysEnabled, // important! Versions: version.MustGetConstraint(">= 9", version.SemanticFormat), // important! outside of the version range }, }, wantContext: Context{ Source: &source.Description{ Metadata: PURLLiteralMetadata{ PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", }, }, }, wantPkgs: []Package{ { Name: "systemd-x", Version: "239-82.el8_10.2", Type: pkg.RpmPkg, PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10", // important! we are NOT patching the channel into the PURL Distro: &distro.Distro{Type: distro.RedHat, Version: "8.10", IDLike: []string{"redhat"}}, // important! channel NOT applied because outside of version range }, }, }, { name: "fails on purl list input", userInput: "purl:testdata/purl/invalid-purl.txt", channels: testFixChannels(), wantErr: require.Error, }, { name: "invalid prefix", userInput: "dir:testdata/purl", channels: testFixChannels(), wantErr: require.Error, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } packages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{}, getDistroChannelApplier(tc.channels)) tc.wantErr(t, err) if err != nil { require.Nil(t, packages) return } if d := cmp.Diff(tc.wantContext, ctx, diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } require.Len(t, packages, len(tc.wantPkgs)) for idx, expected := range tc.wantPkgs { if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } } }) } } func names(ns ...string) []string { return ns } ================================================ FILE: grype/pkg/qualifier/platformcpe/qualifier.go ================================================ package platformcpe import ( "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/syft/syft/cpe" ) type platformCPE struct { cpe string } func New(cpe string) qualifier.Qualifier { return &platformCPE{cpe: cpe} } func isWindowsPlatformCPE(c cpe.CPE) bool { return c.Attributes.Vendor == "microsoft" && strings.HasPrefix(c.Attributes.Product, "windows") } func isUbuntuPlatformCPE(c cpe.CPE) bool { if c.Attributes.Vendor == "canonical" && c.Attributes.Product == "ubuntu_linux" { return true } if c.Attributes.Vendor == "ubuntu" { return true } return false } func isDebianPlatformCPE(c cpe.CPE) bool { return c.Attributes.Vendor == "debian" && (c.Attributes.Product == "debian_linux" || c.Attributes.Product == "linux") } func isWordpressPlatformCPE(c cpe.CPE) bool { return c.Attributes.Vendor == "wordpress" && c.Attributes.Product == "wordpress" } func (p platformCPE) Satisfied(pk pkg.Package) (bool, error) { if p.cpe == "" { return true, nil } c, err := cpe.New(p.cpe, "") if err != nil { return true, err } // NOTE: if syft ever supports cataloging wordpress plugins there will need to be a // package type condition check added here if isWordpressPlatformCPE(c) { return false, nil } // The remaining checks are on distro, so if the distro is unknown the condition should // be considered to be satisfied and avoid filtering matches if pk.Distro == nil { return true, nil } if isWindowsPlatformCPE(c) { return pk.Distro.Type == distro.Windows, nil } if isUbuntuPlatformCPE(c) { return pk.Distro.Type == distro.Ubuntu, nil } if isDebianPlatformCPE(c) { return pk.Distro.Type == distro.Debian, nil } return true, err } ================================================ FILE: grype/pkg/qualifier/platformcpe/qualifier_test.go ================================================ package platformcpe import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" ) func TestPlatformCPE_Satisfied(t *testing.T) { tests := []struct { name string platformCPE qualifier.Qualifier pkg pkg.Package satisfied bool hasError bool }{ { name: "no filter on nil distro", platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"), pkg: pkg.Package{}, satisfied: true, hasError: false, }, { name: "no filter when platform CPE is empty", platformCPE: New(""), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Windows}, }, satisfied: true, hasError: false, }, { name: "no filter when platform CPE is invalid", platformCPE: New(";;;"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Windows}, }, satisfied: true, hasError: true, }, // Windows { name: "filter windows platform vuln when distro is not windows", platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Debian}, }, satisfied: false, hasError: false, }, { name: "filter windows server platform vuln when distro is not windows", platformCPE: New("cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Debian}, }, satisfied: false, hasError: false, }, { name: "no filter windows platform vuln when distro is windows", platformCPE: New("cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Windows}, }, satisfied: true, hasError: false, }, { name: "no filter windows server platform vuln when distro is windows", platformCPE: New("cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Windows}, }, satisfied: true, hasError: false, }, // Debian { name: "filter debian platform vuln when distro is not debian", platformCPE: New("cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Ubuntu}, }, satisfied: false, hasError: false, }, { name: "filter debian platform vuln when distro is not debian (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.SLES}, }, satisfied: false, hasError: false, }, { name: "no filter debian platform vuln when distro is debian", platformCPE: New("cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Debian}, }, satisfied: true, hasError: false, }, { name: "no filter debian platform vuln when distro is debian (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Debian}, }, satisfied: true, hasError: false, }, // Ubuntu { name: "filter ubuntu platform vuln when distro is not ubuntu", platformCPE: New("cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.SLES}, }, satisfied: false, hasError: false, }, { name: "filter ubuntu platform vuln when distro is not ubuntu (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Alpine}, }, satisfied: false, hasError: false, }, { name: "no filter ubuntu platform vuln when distro is ubuntu", platformCPE: New("cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Ubuntu}, }, satisfied: true, hasError: false, }, { name: "no filter ubuntu platform vuln when distro is ubuntu (alternate encountered cpe)", platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Ubuntu}, }, satisfied: true, hasError: false, }, // Wordpress { name: "always filter wordpress platform vulns (no known distro)", platformCPE: New("cpe:2.3:o:wordpress:wordpress:-:*:*:*:*:*:*:*"), pkg: pkg.Package{}, satisfied: false, hasError: false, }, { name: "always filter wordpress platform vulns (known distro)", platformCPE: New("cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*"), pkg: pkg.Package{ Distro: &distro.Distro{Type: distro.Alpine}, }, satisfied: false, hasError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { s, err := test.platformCPE.Satisfied(test.pkg) if test.hasError { assert.Error(t, err) } else { assert.NoError(t, err) } assert.Equal(t, test.satisfied, s) }) } } ================================================ FILE: grype/pkg/qualifier/qualifier.go ================================================ package qualifier import ( "github.com/anchore/grype/grype/pkg" ) type Qualifier interface { Satisfied(p pkg.Package) (bool, error) } ================================================ FILE: grype/pkg/qualifier/rpmmodularity/qualifier.go ================================================ package rpmmodularity import ( "strings" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" ) type rpmModularity struct { module string } func New(module string) qualifier.Qualifier { return &rpmModularity{module: module} } func (r rpmModularity) Satisfied(p pkg.Package) (bool, error) { if p.Metadata == nil { // If unable to determine package modularity, the constraint should be considered satisfied return true, nil } m, ok := p.Metadata.(pkg.RpmMetadata) if !ok { return false, nil } if m.ModularityLabel == nil { // If the package modularity is empty (null), the constraint should be considered satisfied. // this is the case where the package source does not support expressing modularity. return true, nil } if p.Distro != nil && p.Distro.Type == distro.OracleLinux && *m.ModularityLabel == "" { // For oraclelinux, the default stream of an installed appstream package does not currently set // the MODULARITYLABEL property in the rpm metadata; however, in their advisory data they do specify // modularity information, so this ends up in a case where the vuln entries have modularity but the // packages coming from the sbom won't, so for now we need to treat the constraint as satisfied when the // modularity label from an oraclelinux package is "". // TODO: remove this once we have a way of obtaining and parsing the module information from the DISTTAG // in syft. return true, nil } if r.module == "" { if *m.ModularityLabel == "" { // the DB has a modularity label, but it's empty... we also have a modularity label from a package source // that supports being able to express modularity, but it's empty. This is a match. return true, nil } // The package source is able to express modularity, and the DB has a package quality that is empty. // Since we are doing a prefix match against the modularity label (which is guaranteed to be non-empty), // and we are checking for an empty prefix, this will always match, however, semantically this makes no sense. // We don't want package modularities of any value to match this qualifier. return false, nil } return strings.HasPrefix(*m.ModularityLabel, r.module), nil } ================================================ FILE: grype/pkg/qualifier/rpmmodularity/qualifier_test.go ================================================ package rpmmodularity import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" ) func TestRpmModularity_Satisfied(t *testing.T) { oracle := distro.New(distro.OracleLinux, "8", "") tests := []struct { name string rpmModularity qualifier.Qualifier pkg pkg.Package satisfied bool }{ { name: "non rpm metadata", rpmModularity: New("test:1"), pkg: pkg.Package{ Distro: nil, Metadata: pkg.JavaMetadata{}, }, satisfied: false, }, { name: "module with package rpm metadata lacking actual metadata 1", rpmModularity: New("test:1"), pkg: pkg.Package{ Distro: nil, Metadata: nil, }, satisfied: true, }, { name: "empty module with rpm metadata lacking actual metadata 2", rpmModularity: New(""), pkg: pkg.Package{Metadata: nil}, satisfied: true, }, { name: "no modularity label with no module", rpmModularity: New(""), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ Epoch: nil, }}, satisfied: true, }, { name: "no modularity label with module", rpmModularity: New("abc"), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ Epoch: nil, }}, satisfied: true, }, { name: "modularity label with no module", rpmModularity: New(""), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("x:3:1234567:abcd"), }}, satisfied: false, }, { name: "modularity label in module", rpmModularity: New("x:3"), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("x:3:1234567:abcd"), }}, satisfied: true, }, { name: "modularity label not in module", rpmModularity: New("x:3"), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("x:1:1234567:abcd"), }}, satisfied: false, }, { name: "modularity label is positively blank", rpmModularity: New(""), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef(""), }}, satisfied: true, }, { name: "modularity label is missing (assume we cannot verify that capability)", rpmModularity: New(""), pkg: pkg.Package{ Distro: nil, Metadata: pkg.RpmMetadata{ ModularityLabel: nil, }}, satisfied: true, }, { name: "default appstream for oraclelinux (treat as missing)", rpmModularity: New("nodejs:16"), pkg: pkg.Package{ Distro: oracle, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef(""), }}, satisfied: true, }, { name: "non-default appstream for oraclelinux matches vuln modularity", rpmModularity: New("nodejs:16"), pkg: pkg.Package{ Distro: oracle, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("nodejs:16:blah"), }}, satisfied: true, }, { name: "non-default appstream for oraclelinux does not match vuln modularity", rpmModularity: New("nodejs:17"), pkg: pkg.Package{ Distro: oracle, Metadata: pkg.RpmMetadata{ ModularityLabel: strRef("nodejs:16:blah"), }}, satisfied: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { s, err := test.rpmModularity.Satisfied(test.pkg) assert.NoError(t, err) assert.Equal(t, test.satisfied, s) }) } } func strRef(s string) *string { return &s } ================================================ FILE: grype/pkg/rpm_metadata.go ================================================ package pkg type RpmMetadata struct { Epoch *int `json:"epoch" cyclonedx:"epoch"` ModularityLabel *string `json:"modularityLabel" cyclonedx:"modularityLabel"` } ================================================ FILE: grype/pkg/syft_provider.go ================================================ package pkg import ( "context" "errors" "github.com/anchore/go-collections" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal/log" "github.com/anchore/stereoscope" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/sourceproviders" ) func syftProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) { src, err := getSource(userInput, config) if err != nil { return nil, Context{}, nil, err } defer log.CloseAndLogError(src, "syft source") s, err := syft.CreateSBOM(context.Background(), src, config.SBOMOptions) if err != nil { return nil, Context{}, nil, err } if s == nil { return nil, Context{}, nil, errors.New("no SBOM provided") } srcDescription := src.Describe() d, distroDetectionFailed := distroFromSBOM(s, config, applyChannel) pkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d) packages := FromCollection(pkgCatalog, config.SynthesisConfig) pkgCtx := Context{ Source: &srcDescription, Distro: d, DistroDetectionFailed: distroDetectionFailed, } return packages, pkgCtx, s, nil } func distroFromSBOM(s *sbom.SBOM, config ProviderConfig, applyChannel func(*distro.Distro) bool) (d *distro.Distro, detectionFailed bool) { if config.Distro.Override != nil { d = config.Distro.Override } else { d = distro.FromRelease(s.Artifacts.LinuxDistribution, config.Distro.FixChannels) applyChannel(d) // detection failed if we had linux release info but couldn't determine distro type detectionFailed = s.Artifacts.LinuxDistribution != nil && d == nil } return d, detectionFailed } func getSource(userInput string, config ProviderConfig) (source.Source, error) { if config.SBOMOptions.Search.Scope == "" { return nil, errDoesNotProvide } var err error var platform *image.Platform if config.Platform != "" { platform, err = image.NewPlatform(config.Platform) if err != nil { return nil, err } } // prioritize explicitly specified sources from --from flag sources := config.Sources if len(sources) == 0 { // fallback to extracting from scheme if --from not specified (for backward compatibility) schemeSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceTags()...) if schemeSource != "" { sources = []string{schemeSource} userInput = newUserInput } } return syft.GetSource(context.Background(), userInput, syft.DefaultGetSourceConfig(). WithSources(sources...). WithDefaultImagePullSource(config.DefaultImagePullSource). WithAlias(source.Alias{Name: config.Name}). WithRegistryOptions(config.RegistryOptions). WithPlatform(platform). WithExcludeConfig(source.ExcludeConfig{Paths: config.Exclusions})) } func allSourceTags() []string { return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags() } ================================================ FILE: grype/pkg/syft_sbom_provider.go ================================================ package pkg import ( "bytes" "errors" "fmt" "io" "os" "strings" "github.com/gabriel-vasile/mimetype" "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/format/syftjson" "github.com/anchore/syft/syft/sbom" ) type SBOMFileMetadata struct { Path string } func syftSBOMProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) { s, fmtID, path, err := getSBOM(userInput) if err != nil { return nil, Context{}, nil, err } src := s.Source if src.Metadata == nil && path != "" { src.Metadata = SBOMFileMetadata{ Path: path, } } d, distroDetectionFailed := distroFromSBOM(s, config, applyChannel) catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d) var enhancers []Enhancer if fmtID != syftjson.ID { enhancers = purlEnhancers(applyChannel) } return FromCollection(catalog, config.SynthesisConfig, enhancers...), Context{ Source: &src, Distro: d, DistroDetectionFailed: distroDetectionFailed, }, s, nil } func getSBOM(userInput string) (*sbom.SBOM, sbom.FormatID, string, error) { reader, path, err := getSBOMReader(userInput) if err != nil { return nil, "", path, err } s, fmtID, err := readSBOM(reader) return s, fmtID, path, err } func readSBOM(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, error) { s, fmtID, _, err := format.Decode(reader) if err != nil { return nil, "", fmt.Errorf("unable to decode sbom: %w", err) } if fmtID == "" || s == nil { return nil, "", errDoesNotProvide } return s, fmtID, nil } func getSBOMReader(userInput string) (io.ReadSeeker, string, error) { switch { // the order of cases matter case userInput == "": // we only want to attempt reading in from stdin if the user has not specified other // options from the CLI, otherwise we should not assume there is any valid input from stdin. r, err := stdinReader() if err != nil { return nil, "", err } return decodeStdin(r) case explicitlySpecifyingPurlList(userInput): filepath := strings.TrimPrefix(userInput, purlInputPrefix) return openFile(filepath) case explicitlySpecifyingCPEList(userInput): filepath := strings.TrimPrefix(userInput, cpeListPrefix) return openFile(filepath) case explicitlySpecifyingSBOM(userInput): filepath := strings.TrimPrefix(userInput, "sbom:") return openFile(filepath) case isPossibleSBOM(userInput): return openFile(userInput) default: return nil, "", errDoesNotProvide } } func decodeStdin(r io.Reader) (io.ReadSeeker, string, error) { b, err := io.ReadAll(r) if err != nil { return nil, "", fmt.Errorf("failed reading stdin: %w", err) } reader := bytes.NewReader(b) _, err = reader.Seek(0, io.SeekStart) if err != nil { return nil, "", fmt.Errorf("failed to parse stdin: %w", err) } return reader, "", nil } func stdinReader() (io.Reader, error) { isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() if err != nil { return nil, fmt.Errorf("unable to determine if there is piped input: %w", err) } if !isStdinPipeOrRedirect { return nil, errors.New("no input was provided via stdin") } return os.Stdin, nil } func openFile(path string) (io.ReadSeekCloser, string, error) { expandedPath, err := homedir.Expand(path) if err != nil { return nil, path, fmt.Errorf("unable to open SBOM: %w", err) } f, err := os.Open(expandedPath) if err != nil { return nil, path, fmt.Errorf("unable to open file %s: %w", expandedPath, err) } return f, path, nil } func isPossibleSBOM(userInput string) bool { f, path, err := openFile(userInput) if err != nil { return false } defer log.CloseAndLogError(f, path) mType, err := mimetype.DetectReader(f) if err != nil { return false } // we expect application/json, application/xml, and text/plain input documents. All of these are either // text/plain or a descendant of text/plain. Anything else cannot be an input SBOM document. return isAncestorOfMimetype(mType, "text/plain") } func isAncestorOfMimetype(mType *mimetype.MIME, expected string) bool { for cur := mType; cur != nil; cur = cur.Parent() { if cur.Is(expected) { return true } } return false } func explicitlySpecifyingSBOM(userInput string) bool { return strings.HasPrefix(userInput, "sbom:") } func explicitlySpecifyingPurlList(userInput string) bool { return strings.HasPrefix(userInput, purlInputPrefix) } func explicitlySpecifyingCPEList(userInput string) bool { return strings.HasPrefix(userInput, cpeListPrefix) } ================================================ FILE: grype/pkg/syft_sbom_provider_test.go ================================================ package pkg import ( "slices" "strings" "testing" "github.com/go-test/deep" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) func TestParseSyftJSON(t *testing.T) { applyChannel := getDistroChannelApplier(testFixChannels()) tests := []struct { Fixture string Packages []Package Context Context }{ { Fixture: "testdata/syft-multiple-ecosystems.json", Packages: []Package{ { Name: "alpine-baselayout", Version: "3.2.0-r6", Locations: file.NewLocationSet( file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759", }), ), Language: "", Licenses: []string{ "GPL-2.0-only", }, Type: "apk", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r6:*:*:*:*:*:*:*", ""), }, PURL: "pkg:alpine/alpine-baselayout@3.2.0-r6?arch=x86_64", Upstreams: []UpstreamPackage{ { Name: "alpine-baselayout", }, }, Metadata: ApkMetadata{ Files: []ApkFileRecord{ {Path: "/dev"}, {Path: "/dev/pts"}, {Path: "/dev/shm"}, {Path: "/etc"}, {Path: "/etc/fstab"}, {Path: "/etc/group"}, {Path: "/etc/hostname"}, {Path: "/etc/hosts"}, {Path: "/etc/inittab"}, {Path: "/etc/modules"}, {Path: "/etc/motd"}, {Path: "/etc/mtab"}, {Path: "/etc/passwd"}, {Path: "/etc/profile"}, {Path: "/etc/protocols"}, {Path: "/etc/services"}, {Path: "/etc/shadow"}, {Path: "/etc/shells"}, {Path: "/etc/sysctl.conf"}, {Path: "/etc/apk"}, {Path: "/etc/conf.d"}, {Path: "/etc/crontabs"}, {Path: "/etc/crontabs/root"}, {Path: "/etc/init.d"}, {Path: "/etc/modprobe.d"}, {Path: "/etc/modprobe.d/aliases.conf"}, {Path: "/etc/modprobe.d/blacklist.conf"}, {Path: "/etc/modprobe.d/i386.conf"}, {Path: "/etc/modprobe.d/kms.conf"}, {Path: "/etc/modules-load.d"}, {Path: "/etc/network"}, {Path: "/etc/network/if-down.d"}, {Path: "/etc/network/if-post-down.d"}, {Path: "/etc/network/if-pre-up.d"}, {Path: "/etc/network/if-up.d"}, {Path: "/etc/opt"}, {Path: "/etc/periodic"}, {Path: "/etc/periodic/15min"}, {Path: "/etc/periodic/daily"}, {Path: "/etc/periodic/hourly"}, {Path: "/etc/periodic/monthly"}, {Path: "/etc/periodic/weekly"}, {Path: "/etc/profile.d"}, {Path: "/etc/profile.d/README"}, {Path: "/etc/profile.d/color_prompt.sh.disabled"}, {Path: "/etc/profile.d/locale.sh"}, {Path: "/etc/sysctl.d"}, {Path: "/home"}, {Path: "/lib"}, {Path: "/lib/firmware"}, {Path: "/lib/mdev"}, {Path: "/lib/modules-load.d"}, {Path: "/lib/sysctl.d"}, {Path: "/lib/sysctl.d/00-alpine.conf"}, {Path: "/media"}, {Path: "/media/cdrom"}, {Path: "/media/floppy"}, {Path: "/media/usb"}, {Path: "/mnt"}, {Path: "/opt"}, {Path: "/proc"}, {Path: "/root"}, {Path: "/run"}, {Path: "/sbin"}, {Path: "/sbin/mkmntdirs"}, {Path: "/srv"}, {Path: "/sys"}, {Path: "/tmp"}, {Path: "/usr"}, {Path: "/usr/lib"}, {Path: "/usr/lib/modules-load.d"}, {Path: "/usr/local"}, {Path: "/usr/local/bin"}, {Path: "/usr/local/lib"}, {Path: "/usr/local/share"}, {Path: "/usr/sbin"}, {Path: "/usr/share"}, {Path: "/usr/share/man"}, {Path: "/usr/share/misc"}, {Path: "/var"}, {Path: "/var/run"}, {Path: "/var/cache"}, {Path: "/var/cache/misc"}, {Path: "/var/empty"}, {Path: "/var/lib"}, {Path: "/var/lib/misc"}, {Path: "/var/local"}, {Path: "/var/lock"}, {Path: "/var/lock/subsys"}, {Path: "/var/log"}, {Path: "/var/mail"}, {Path: "/var/opt"}, {Path: "/var/spool"}, {Path: "/var/spool/mail"}, {Path: "/var/spool/cron"}, {Path: "/var/spool/cron/crontabs"}, {Path: "/var/tmp"}, }, }, }, { Name: "fake", Version: "1.2.0", Locations: file.NewLocationSet( file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), ), Language: "lang", Licenses: []string{ "LGPL-3.0-or-later", }, Type: "dpkg", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:fake:1.2.0:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:a:fake:fake:1.2.0:*:*:*:*:*:*:*", ""), }, PURL: "pkg:deb/debian/fake@1.2.0?arch=x86_64", Upstreams: []UpstreamPackage{ { Name: "a-source", Version: "1.4.5", }, }, }, { Name: "gmp", Version: "6.2.0-r0", Locations: file.NewLocationSet( file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), ), Language: "the-lang", Licenses: []string{ "LGPL-3.0-or-later", }, Type: "java-archive", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:*:gmp:6.2.0-r0:*:*:*:*:*:*:*", ""), cpe.Must("cpe:2.3:a:gmp:gmp:6.2.0-r0:*:*:*:*:*:*:*", ""), }, PURL: "pkg:alpine/gmp@6.2.0-r0?arch=x86_64", Metadata: JavaMetadata{ PomArtifactID: "aid", PomGroupID: "gid", ManifestName: "a-name", }, }, }, Context: Context{ Source: &source.Description{ Metadata: source.ImageMetadata{ UserInput: "alpine:fake", Layers: []source.LayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a", Size: 5570176, }, }, Size: 15879684, ID: "sha256:fadf1294c09213b20d4d6fc84109584e1c102d185c2cae15144a87d29de65c6d", ManifestDigest: "sha256:1f6495428fb363e2d233e5df078b2b200635c4e51f0a3be34ecf09d44b547590", MediaType: "application/vnd.docker.distribution.manifest.v2+json", Tags: []string{ "alpine:fake", }, }, }, Distro: &distro.Distro{ Type: "alpine", Version: "3.12.0", }, }, }, springImageTestCase, } for _, test := range tests { t.Run(test.Fixture, func(t *testing.T) { pkgs, context, _, err := syftSBOMProvider(test.Fixture, ProviderConfig{}, applyChannel) if err != nil { t.Fatalf("unable to parse: %+v", err) } if m, ok := context.Source.Metadata.(source.ImageMetadata); ok { m.RawConfig = nil m.RawManifest = nil context.Source.Metadata = m } for _, d := range deep.Equal(test.Packages, pkgs) { if strings.Contains(d, ".ID: ") { // today ID's get assigned by the collection, which will change in the future. But in the meantime // that means that these IDs are random and should not be counted as a difference we care about in // this test. continue } t.Errorf("pkg diff: %s", d) } for _, d := range deep.Equal(test.Context, context) { if strings.Contains(d, "Distro.IDLike: != []") { continue } t.Errorf("ctx diff: %s", d) } }) } } func TestParseSyftJSON_BadCPEs(t *testing.T) { applyChannel := getDistroChannelApplier(testFixChannels()) pkgs, _, _, err := syftSBOMProvider("testdata/syft-java-bad-cpes.json", ProviderConfig{}, applyChannel) assert.NoError(t, err) assert.Len(t, pkgs, 1) } // Note that the fixture has been modified from the real syft output to include fewer packages, CPEs, layers, // and package IDs are removed so that the test case variable isn't unwieldingly huge. var springImageTestCase = struct { Fixture string Packages []Package Context Context }{ Fixture: "testdata/syft-spring.json", Packages: []Package{ { Name: "charsets", Version: "", Locations: file.NewLocationSet( file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar", FileSystemID: "sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393", }), ), Language: "java", Licenses: []string{}, Type: "java-archive", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:charsets:charsets:*:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:a:charsets:charsets:*:*:*:*:*:maven:*:*", ""), }, PURL: "", Metadata: JavaMetadata{VirtualPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar"}, }, { Name: "tomcat-embed-el", Version: "9.0.27", Locations: file.NewLocationSet( file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/app/libs/tomcat-embed-el-9.0.27.jar", FileSystemID: "sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4", }), ), Language: "java", Licenses: []string{}, Type: "java-archive", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:a:tomcat_embed_el:tomcat-embed-el:9.0.27:*:*:*:*:java:*:*", ""), cpe.Must("cpe:2.3:a:tomcat-embed-el:tomcat_embed_el:9.0.27:*:*:*:*:maven:*:*", ""), }, PURL: "", Metadata: JavaMetadata{VirtualPath: "/app/libs/tomcat-embed-el-9.0.27.jar"}, }, }, Context: Context{ Source: &source.Description{ Metadata: source.ImageMetadata{ UserInput: "springio/gs-spring-boot-docker:latest", Layers: []source.LayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602", Size: 1809479, }, }, Size: 142807921, ID: "sha256:9065659c6e537b0364b7b1d3e5442a3a5aa56d755fb883d221e9e8b3637fb58e", ManifestDigest: "sha256:be3d8a5f700d4c45f3ed324b95d9f028f587c135bc85cf87e193414db521d533", MediaType: "application/vnd.docker.distribution.manifest.v2+json", Tags: []string{ "springio/gs-spring-boot-docker:latest", }, RepoDigests: []string{"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08"}, }, }, Distro: &distro.Distro{ Type: "debian", Version: "9", }, }, } func Test_PurlList(t *testing.T) { tests := []struct { name string config ProviderConfig userInput string wantContext Context wantPkgs []Package wantErr require.ErrorAssertionFunc }{ { name: "takes multiple purls", userInput: "purl:testdata/purl/valid-purl.txt", wantContext: Context{ Distro: &distro.Distro{ Type: "debian", IDLike: []string{"debian"}, Version: "8", }, Source: &source.Description{ Metadata: SBOMFileMetadata{ Path: "testdata/purl/valid-purl.txt", }, }, }, wantPkgs: []Package{ { Name: "ant", Version: "1.10.8", Type: pkg.JavaPkg, PURL: "pkg:maven/org.apache.ant/ant@1.10.8", Distro: &distro.Distro{Type: distro.Debian, Version: "8", IDLike: []string{"debian"}}, Metadata: JavaMetadata{ PomArtifactID: "ant", PomGroupID: "org.apache.ant", }, }, { Name: "log4j-core", Version: "2.14.1", Type: pkg.JavaPkg, PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", Distro: &distro.Distro{Type: distro.Debian, Version: "8", IDLike: []string{"debian"}}, Metadata: JavaMetadata{ PomArtifactID: "log4j-core", PomGroupID: "org.apache.logging.log4j", }, }, { Name: "sysv-rc", Version: "2.88dsf-59", Type: pkg.DebPkg, PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit", Distro: &distro.Distro{Type: distro.Debian, Version: "8", IDLike: []string{"debian"}}, Upstreams: []UpstreamPackage{ { Name: "sysvinit", }, }, }, }, }, { name: "infer context when distro is present for multiple similar purls", userInput: "purl:testdata/purl/homogeneous-os.txt", wantContext: Context{ Distro: &distro.Distro{ Type: "alpine", IDLike: []string{"alpine"}, Version: "3.20.3", }, Source: &source.Description{ Metadata: SBOMFileMetadata{ Path: "testdata/purl/homogeneous-os.txt", }, }, }, wantPkgs: []Package{ { Name: "openssl", Version: "3.2.1", Type: pkg.ApkPkg, PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", IDLike: []string{"alpine"}}, }, { Name: "curl", Version: "7.61.1", Type: pkg.ApkPkg, PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3", Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", IDLike: []string{"alpine"}}, }, }, }, { name: "different distro info in purls does not infer context", userInput: "purl:testdata/purl/different-os.txt", wantContext: Context{ // important: no distro info inferred Source: &source.Description{ Metadata: SBOMFileMetadata{ Path: "testdata/purl/different-os.txt", }, }, }, wantPkgs: []Package{ { Name: "openssl", Version: "3.2.1", Type: pkg.ApkPkg, PURL: "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3", Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.3", IDLike: []string{"alpine"}}, }, { Name: "curl", Version: "7.61.1", Type: pkg.ApkPkg, PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2", Distro: &distro.Distro{Type: distro.Alpine, Version: "3.20.2", IDLike: []string{"alpine"}}, }, }, }, { name: "fails on path with nonexistent file", userInput: "purl:tttt/empty.txt", wantErr: require.Error, }, { name: "fails on invalid path", userInput: "purl:~&&", wantErr: require.Error, }, { name: "fails for empty purl file", userInput: "purl:testdata/purl/empty.json", wantErr: require.Error, }, { name: "fails on invalid purl in file", userInput: "purl:testdata/purl/invalid-purl.txt", wantErr: require.Error, }, { name: "honors default channel configuration (no EUS)", config: ProviderConfig{ SynthesisConfig: SynthesisConfig{ Distro: DistroConfig{ FixChannels: testFixChannels(), }, }, }, userInput: "purl:testdata/purl/valid-rhel-9.txt", wantContext: Context{ Source: &source.Description{ Metadata: SBOMFileMetadata{ Path: "testdata/purl/valid-rhel-9.txt", }, }, Distro: &distro.Distro{ Type: distro.RedHat, IDLike: []string{"redhat"}, Version: "9.4", }, }, wantPkgs: []Package{ { Name: "kernel", Version: "0:5.14.0-100", Type: pkg.RpmPkg, Distro: &distro.Distro{Type: distro.RedHat, Version: "9.4", IDLike: []string{"redhat"}}, PURL: "pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4", }, }, }, { name: "honors default channel configuration (EUS)", config: ProviderConfig{ SynthesisConfig: SynthesisConfig{ Distro: DistroConfig{ FixChannels: testFixChannels(), }, }, }, userInput: "purl:testdata/purl/valid-rhel-9+eus.txt", wantContext: Context{ Source: &source.Description{ Metadata: SBOMFileMetadata{ Path: "testdata/purl/valid-rhel-9+eus.txt", }, }, Distro: &distro.Distro{ Type: distro.RedHat, IDLike: []string{"redhat"}, Channels: names("eus"), // important! Version: "9.4", }, }, wantPkgs: []Package{ { Name: "kernel", Version: "0:5.14.0-100", Type: pkg.RpmPkg, Distro: &distro.Distro{Type: distro.RedHat, Version: "9.4", Channels: names("eus"), IDLike: []string{"redhat"}}, PURL: "pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4+eus", }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } packages, ctx, _, err := Provide(tc.userInput, tc.config) tc.wantErr(t, err) if err != nil { require.Nil(t, packages) return } if d := cmp.Diff(tc.wantContext, ctx, diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } require.Len(t, packages, len(tc.wantPkgs)) slices.SortFunc(packages, func(a, b Package) int { return strings.Compare(a.Name, b.Name) }) slices.SortFunc(tc.wantPkgs, func(a, b Package) int { return strings.Compare(a.Name, b.Name) }) for idx, expected := range tc.wantPkgs { if d := cmp.Diff(expected, packages[idx], diffOpts...); d != "" { t.Errorf("unexpected context (-want +got):\n%s", d) } } }) } } func testFixChannels() []distro.FixChannel { return distro.DefaultFixChannels() } ================================================ FILE: grype/pkg/testdata/alpine-tampered.att.json ================================================ {"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3N5ZnQuZGV2L2JvbSIsInN1YmplY3QiOlt7Im5hbWUiOiIiLCJkaWdlc3QiOnsic2hhMjU2IjoiNmFmMWIxMWJiYjE3ZjRjMzExZTI2OWRiNjUzMGU0ZGEyNzM4MjYyYWY1ZmQ5MDY0Y2NkZjEwOWI3NjU4NjBmYiJ9fV0sInByZWRpY2F0ZSI6eyJhcnRpZmFjdFJlbGF0aW9uc2hpcHMiOlt7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWZhOTRlYThlODc0ZWU0OSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNTU5YTlkNTFlMzNkMjQxMCIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM2Zjk4Yzg1N2E2OTFjYyIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYzc5NWQzZjdlYmQ2NGU4NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGYyMmVkMWEzMWZkYmI2OCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTg1YWYxNGQ1MDMyNTc0NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGJlNzE2ZTA1ZjkxMWZmOSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWEzNTc2ZTRhYWRiODJmMSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNWEyNWQwZGJlODBhYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1NTU4Zjc2MDM4ZDFjOTdlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzODcxMmFkZWFlZGI3MzE2IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGRkOTg1ZGNlNGNkNWE1IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OTBjYzI2MzU4N2UyYzlkIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyNmEyMGYxMDZmYTMyNzgzIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmMjlhZjI0NzU1MGRjYWM4IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTZkNmUwZWE5NTc1MWI3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiOGYxZTRhNjJiMjE2NjNiIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0NTZhNzAyZjE2NzMzYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGUzZjQxYTc3YjQyODA3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNDFkYWQ4YTYxZmY4M2I2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjOWJlYzUzYzBhM2UyMTA2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNWRmZDI5MTQ4MWM2OWUwIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjMTVmOTJkZGM3N2MxYWU0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkMGMyNDUzZjk4YjMxMWQ4IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYzY2MzYyYjI1YzY0MzRhIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzVjYmI3OGNjNzQwZmJlIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkZWMyMzliNTY1ZDE5YjY0IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNjA1NDJmYTMzZGFlYjA1IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyZDI0YzQwYTg4YjRjMmVjIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1ZGIzOGRkY2U4OTEwNWU3IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwicGFyZW50IjoiYzJlM2NlN2I5ZTcyZDBhZSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4NTI0ZGNmMDI5MzZkODE3IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYzYyNGZiZjM5Y2E3ZDY0IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYWY0MTJiMTBmZjJjNWZiIiwicGFyZW50IjoiMWYyOGRlMTIwMDczZDc4YSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyMzcwMjZkMTM0Yjg0ZDZmIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6IjNjYTdjYmZkNTkzMGQ5MGIiLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWFjYzQ2ZjlmOWQyNDA1OCIsInBhcmVudCI6IjUzYTkwOWY0ZDg3MmI5MCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNjJmMzI0MDM2NTYxNjQzIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6ImEyOWI0NWI1YzAyZDAwMDciLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifV0sImFydGlmYWN0cyI6W3siY3BlcyI6WyJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI3ZjAyNDFhNzBiNjgxODE5IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImZpbGVzIjpbeyJwYXRoIjoiL2RldiJ9LHsicGF0aCI6Ii9kZXYvcHRzIn0seyJwYXRoIjoiL2Rldi9zaG0ifSx7InBhdGgiOiIvZXRjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTExUTdoTmU4UXBEUzUzMWd1cUNkclhCem9BL289In0sInBhdGgiOiIvZXRjL2ZzdGFiIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEzSytvbEpnNWF5ekhTVk5Va2dnWkpYdUIrOVk9In0sInBhdGgiOiIvZXRjL2dyb3VwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE2blZ3WVZYUC90Q2h2VVBkdWtWRDJpZlhPbWM9In0sInBhdGgiOiIvZXRjL2hvc3RuYW1lIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFCRDZ6SktaVFJXeXFHblBpNHRTZmQza3JzTVU9In0sInBhdGgiOiIvZXRjL2hvc3RzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFUc3RoYmhXN1F6V1JlMUUvTkt3VE91RDRwSGM9In0sInBhdGgiOiIvZXRjL2luaXR0YWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXRvb2dqVWlwSEdjTWdFQ2dQSlg2NFN3VVQxTT0ifSwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExWG1kdVZWTlVSSFEyN1R2WXAxTHI1VE10RmNBPSJ9LCJwYXRoIjoiL2V0Yy9tb3RkIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFraWxqaFhYSDFMbFFyb0hzRUpJa1BaZzJlaXc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL210YWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVGNodXVMVWZ1cjBpenZmWlFaeGdOL0xKaEI4PSJ9LCJwYXRoIjoiL2V0Yy9wYXNzd2QifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVZtSFBXUGpqdno0b0NzYm1ZQ1VCNHVXcFNrYz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExb21LbHAzdmdHcTJacVl6eUQvS0hOZG84ckRjPSJ9LCJwYXRoIjoiL2V0Yy9wcm90b2NvbHMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTlXTEN2NUl0S2c0TUg3UldmTlJoMUk3YnlRYz0ifSwicGF0aCI6Ii9ldGMvc2VydmljZXMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWx0clBJQVcyekhlRGlhanNleDJCZG1xM3VxQT0ifSwib3duZXJHaWQiOiI0MiIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL3NoYWRvdyIsInBlcm1pc3Npb25zIjoiNjQwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFvam0yWWRwQ0o2Qi9hcEdEYVovU2RiMnhKa0E9In0sInBhdGgiOiIvZXRjL3NoZWxscyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExNHVwejN0Zm5OeFprSUVzVWhXbjdYb2l3OTZnPSJ9LCJwYXRoIjoiL2V0Yy9zeXNjdGwuY29uZiJ9LHsicGF0aCI6Ii9ldGMvYXBrIn0seyJwYXRoIjoiL2V0Yy9jb25mLmQifSx7InBhdGgiOiIvZXRjL2Nyb250YWJzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF2ZmsxYXBVV0k0eUxKR2hoTlJkMGtKaXhmdlk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QiLCJwZXJtaXNzaW9ucyI6IjYwMCJ9LHsicGF0aCI6Ii9ldGMvaW5pdC5kIn0seyJwYXRoIjoiL2V0Yy9tb2Rwcm9iZS5kIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFXVWJoNlRCWU5WSzdlNFkrdVV2THMvN3ZpcWs9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE0VGRnRkhrVGR0M3VRQytOQnRybnRPbm05bjQ9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYmxhY2tsaXN0LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXBuYXkvbmpuNm9sOWNDc3NMN0tpWlo4ZXRsYz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9pMzg2LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXluYkxuM0dZRHB2YWpiYS9sZHAxbmlheWVvZz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9LHsicGF0aCI6Ii9ldGMvbW9kdWxlcy1sb2FkLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQifSx7InBhdGgiOiIvZXRjL29wdCJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMifSx7InBhdGgiOiIvZXRjL3BlcmlvZGljLzE1bWluIn0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9kYWlseSJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMvaG91cmx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9tb250aGx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy93ZWVrbHkifSx7InBhdGgiOiIvZXRjL3Byb2ZpbGUuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMzVPV3NDenp2bkIyZm1GeDYya2JxbTFBeDFrPSJ9LCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvUkVBRE1FIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEwd0wyM0d1U0NWZnVtTVJnYWthYlVJNkVzU2s9In0sInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9jb2xvcl9wcm9tcHQuc2guZGlzYWJsZWQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVM4aitXVzcxbVd4ZlZ5OHl0aHFVN0hVVm9Cdz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9LHsicGF0aCI6Ii9ldGMvc3lzY3RsLmQifSx7InBhdGgiOiIvaG9tZSJ9LHsicGF0aCI6Ii9saWIifSx7InBhdGgiOiIvbGliL2Zpcm13YXJlIn0seyJwYXRoIjoiL2xpYi9tZGV2In0seyJwYXRoIjoiL2xpYi9tb2R1bGVzLWxvYWQuZCJ9LHsicGF0aCI6Ii9saWIvc3lzY3RsLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUhwRWx6VzF4RWdtS2ZFUnRUeTdvb21tbnE2Yz0ifSwicGF0aCI6Ii9saWIvc3lzY3RsLmQvMDAtYWxwaW5lLmNvbmYifSx7InBhdGgiOiIvbWVkaWEifSx7InBhdGgiOiIvbWVkaWEvY2Ryb20ifSx7InBhdGgiOiIvbWVkaWEvZmxvcHB5In0seyJwYXRoIjoiL21lZGlhL3VzYiJ9LHsicGF0aCI6Ii9tbnQifSx7InBhdGgiOiIvb3B0In0seyJwYXRoIjoiL3Byb2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvcm9vdCIsInBlcm1pc3Npb25zIjoiNzAwIn0seyJwYXRoIjoiL3J1biJ9LHsicGF0aCI6Ii9zYmluIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFxamtkeVJKY1libEdDNlJNcVVSNEJkYjVnMTA9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvc2Jpbi9ta21udGRpcnMiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii9zcnYifSx7InBhdGgiOiIvc3lzIn0seyJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsicGF0aCI6Ii91c3IvbGliL21vZHVsZXMtbG9hZC5kIn0seyJwYXRoIjoiL3Vzci9sb2NhbCJ9LHsicGF0aCI6Ii91c3IvbG9jYWwvYmluIn0seyJwYXRoIjoiL3Vzci9sb2NhbC9saWIifSx7InBhdGgiOiIvdXNyL2xvY2FsL3NoYXJlIn0seyJwYXRoIjoiL3Vzci9zYmluIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvbWFuIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9taXNjIn0seyJwYXRoIjoiL3ZhciJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMS9TTlp6LzhjSzJkU0tLK2NKcFZyWkl1RjRRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9ydW4iLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL2VtcHR5IiwicGVybWlzc2lvbnMiOiI1NTUifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL21pc2MifSx7InBhdGgiOiIvdmFyL2xvY2FsIn0seyJwYXRoIjoiL3Zhci9sb2NrIn0seyJwYXRoIjoiL3Zhci9sb2NrL3N1YnN5cyJ9LHsicGF0aCI6Ii92YXIvbG9nIn0seyJwYXRoIjoiL3Zhci9tYWlsIn0seyJwYXRoIjoiL3Zhci9vcHQifSx7InBhdGgiOiIvdmFyL3Nwb29sIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFkemJkYXpZWkEyblR6U0lHM1l5Tnc3ZDRKdWM9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3Nwb29sL21haWwiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvc3Bvb2wvY3JvbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT0ZadCtaTXA3ajBHbnkwcnFTS3VXSnlxWW1BPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9zcG9vbC9jcm9uL2Nyb250YWJzIiwicGVybWlzc2lvbnMiOiI3NzcifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSIsImluc3RhbGxlZFNpemUiOjQxMzY5NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicGFja2FnZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHVsbENoZWNrc3VtIjoiUTFFeW1TNnJBZ21HczdYWWhxZHlFb2lXZ0VaNkE9IiwicHVsbERlcGVuZGVuY2llcyI6Ii9iaW4vc2ggc286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6MjExMDEsInVybCI6Imh0dHBzOi8vZ2l0LmFscGluZWxpbnV4Lm9yZy9jZ2l0L2Fwb3J0cy90cmVlL21haW4vYWxwaW5lLWJhc2VsYXlvdXQiLCJ2ZXJzaW9uIjoiMy4yLjAtcjE4In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHVybCI6InBrZzphbHBpbmUvYWxwaW5lLWJhc2VsYXlvdXRAMy4yLjAtcjE4P2FyY2g9eDg2XzY0JnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0JmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIzLjIuMC1yMTgifSx7ImNwZXMiOlsiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZV9rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZV9rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZV9rZXlzOmFscGluZV9rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmUta2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphbHBpbmU6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiMTk5N2RkMWM4ZmY3MmRjMiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiTUlUIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJQdWJsaWMga2V5cyBmb3IgQWxwaW5lIExpbnV4IHBhY2thZ2VzIiwiZmlsZXMiOlt7InBhdGgiOiIvZXRjIn0seyJwYXRoIjoiL2V0Yy9hcGsifSx7InBhdGgiOiIvZXRjL2Fway9rZXlzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFPdkNGU085NHo5N2M4MG1JREN4cUdraDJPZzQ9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExdjdZV1pZekFXb2NsYUxESTQ1akVndUk3WU4wPSJ9LCJwYXRoIjoiL2V0Yy9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNDNlZjRiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU5uR3VEc2RRT3g0Wk5ZZkIzTjk3ZUx5R1BrST0ifSwicGF0aCI6Ii9ldGMvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjYxY2VjYi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsWmxURVNOcmVsV1ROa0wvb1F6bUFVOGE5OUE9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExV05XNlN5ODdIcEozSWRlbVF5OHBqdTMzS21zPSJ9LCJwYXRoIjoiL2V0Yy9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjY2ZTNmLnJzYS5wdWIifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT3ZDRlNPOTR6OTdjODBtSURDeHFHa2gyT2c0PSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTRhNmEwODQwLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXY3WVdaWXpBV29jbGFMREk0NWpFZ3VJN1lOMD0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjQzZWY0Yi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFCVHFTK0gvVVV5aFF1ekh3aUJsNDcrQlRLdVU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTm5HdURzZFFPeDRaTllmQjNOOTdlTHlHUGtJPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNjFjZWNiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU9heGRjc2E2QVlvUGRMaTBVNGxPM0oyd2UxOD0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ODE5OWRjYy5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF5UHErc3U2NWtzTm94M3VYQitEUjdQMTgrUVU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThjYmI0NzYucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTXBaRE5YMExlTEh2U093VlV5WGlYeDExTk4wPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4ZTRmMTdkLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWdsQ1EvZUpidkE1eHFjc3dkakZyV3Y1Rm5rMD0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ZTY5Y2E1MC5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFYVWRERW9OVHRqbHZyUytpdW5rNnppRmdJcFU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExbFpsVEVTTnJlbFdUTmtML29Rem1BVThhOTlBPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjVlZTU5LnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVdOVzZTeTg3SHBKM0lkZW1ReThwanUzM0ttcz0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTY2NmUzZi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFJOUR5NmhyeWFjTDJZV1hnK0tsRTZXdndFZDQ9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YTk3MjQucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTlNuc2dtY01iVTRnN2o1SmFOczB0VkhwSFZBPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFiYzIzLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVZhTUJCazRSeHY2Ym9QTEtGK0kwODVROHkyRT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhYzNiYy5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEzaEpCTUhBVXF1UGJwNWpwQVBGalFJMlkxdlE9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVi9hNVA5cEtSSmI2dGloRTNlOE82eGFQZ0xVPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFlMzUwLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTN3TEpyY0tRYWpxbDVhMXA5UTQ1VStaWEVOQT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZkYjMwZC5yc2EucHViIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hYXJjaDY0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE3ajluV0prUSt3Zkl1VlF6SUZybUZaN2ZTT2M9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FhcmNoNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ODE5OWRjYy5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNucitRMVViZkh5Q3IvY21tdFZ2TUlTN1NHcz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWFyY2g2NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFlMzUwLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtaGYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVU5UXRzZE4rcllaOVpoNzZFZlh5MDBKWkhNZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtaGYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjRkMjdiYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWJDK0FkUTBxV0JUbWVmWGlJMFB2bVlPSm9WUT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtaGYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhOTcyNC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FybXY3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFVOVF0c2ROK3JZWjlaaDc2RWZYeTAwSlpITWc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FybXY3L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF4YklWdTdTY3dxR0h4WEd3STIyYVNlNU9kVVk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FybXY3L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9taXBzNjQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWhDWmRGeCtMdnpiTHRQczc1M2plNzhnRUVCUT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvbWlwczY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNWU2OWNhNTAucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9wcGM2NGxlIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF0MjFkaENMYlRKbUFIWFNDZU9NcS8ydmZTZ289In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3BwYzY0bGUvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01OGNiYjQ3Ni5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVBTOXpOSVBKYW5DOHFjc2M1cWFyRVdxaFY1UT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcHBjNjRsZS9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFiYzIzLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcmlzY3Y2NCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTlZQYlphdmFYcHNJdEZ3UVlEV2Jwb3I3eVlFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9yaXNjdjY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFVNnRmdUtSeTVKOEM2aWFLUE1aYVQvZTh0YkE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3Jpc2N2NjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZkYjMwZC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3MzOTB4In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFzamJWMnIydzBJaDJ2d2R6QzRKcTZVSTdjTVE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3MzOTB4L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsMDl4YTdSbmJPSUMxZEk5RnFiYUNmUy9HWFk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3MzOTB4L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWMzYmMucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUlpNTFpN05yYzR1ZnQxNEhocXVnYVVxZEg2ND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFZNDllVnhocHZmdGJRM3lBZHZsTGZjclBMVFU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Ni9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNDNlZjRiLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExSGpkdmNWa3BCWnpyMWFTZTNwN29RZkF0bS9FPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTY2NmUzZi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Nl82NCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExSWk1MWk3TnJjNHVmdDE0SGhxdWdhVXFkSDY0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODZfNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy00YTZhMDg0MC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUFVRlkrZndTQlRjcllldGpUN05IdmFmclNRYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2XzY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFxS0EyM1Z6TVVEbGUrRHFucnI1S3orWHZ0eTQ9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Nl82NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjVlZTU5LnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiYWFiNjhmOGM5YWI0MzRhNDY3MTBkZThlMTJmYjMyMDZlMjkzMGE1OSIsImluc3RhbGxlZFNpemUiOjE1OTc0NCwibGljZW5zZSI6Ik1JVCIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6ImFscGluZS1rZXlzIiwicGFja2FnZSI6ImFscGluZS1rZXlzIiwicHVsbENoZWNrc3VtIjoiUTFrREYyc3RLbzNlL1J1bWxBOFpyUmZDd2RTdjg9IiwicHVsbERlcGVuZGVuY2llcyI6IiIsInNpemUiOjEzMzYyLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZyIsInZlcnNpb24iOiIyLjQtcjEifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYWxwaW5lLWtleXMiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjQmdXBzdHJlYW09YWxwaW5lLWtleXMmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjIuNC1yMSJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6YXBrLXRvb2xzOmFway10b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGstdG9vbHM6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFwa190b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YXBrX3Rvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGs6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFwazphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiNWVmNjZhMzM1ZGRjMDNhNiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiR1BMLTIuMC1vbmx5Il0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJBbHBpbmUgUGFja2FnZSBLZWVwZXIgLSBwYWNrYWdlIG1hbmFnZXIgZm9yIGFscGluZSIsImZpbGVzIjpbeyJwYXRoIjoiL2V0YyJ9LHsicGF0aCI6Ii9ldGMvYXBrIn0seyJwYXRoIjoiL2V0Yy9hcGsva2V5cyJ9LHsicGF0aCI6Ii9ldGMvYXBrL3Byb3RlY3RlZF9wYXRocy5kIn0seyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExUzVETDZERk9tampOeEFHTnNzZmo0blVpOFhVPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJhcGsuc28uMy4xMi4wIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvc2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTzFwQlVCMmtUTS9McUw4ZDBUOThDRWJaWXF3PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3NiaW4vYXBrIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvdmFyIn0seyJwYXRoIjoiL3Zhci9jYWNoZSJ9LHsicGF0aCI6Ii92YXIvY2FjaGUvbWlzYyJ9LHsicGF0aCI6Ii92YXIvbGliIn0seyJwYXRoIjoiL3Zhci9saWIvYXBrIn1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiIxYWMzYzFiYjI5ZWVmZjA4M2M2MjFjZjZiMjdhZDEyYWI5M2NiNzNhIiwiaW5zdGFsbGVkU2l6ZSI6MzExMjk2LCJsaWNlbnNlIjoiR1BMLTIuMC1vbmx5IiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgPG5jb3BhQGFscGluZWxpbnV4Lm9yZz4iLCJvcmlnaW5QYWNrYWdlIjoiYXBrLXRvb2xzIiwicGFja2FnZSI6ImFway10b29scyIsInB1bGxDaGVja3N1bSI6IlExM2ZQZCtGUlhhTHd5TmtsVm4rcXVGV0R5a25NPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJtdXNsPj0xLjIgY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIHNvOmxpYnouc28uMSIsInNpemUiOjEyMDM3NywidXJsIjoiaHR0cHM6Ly9naXRsYWIuYWxwaW5lbGludXgub3JnL2FscGluZS9hcGstdG9vbHMiLCJ2ZXJzaW9uIjoiMi4xMi43LXIzIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImFway10b29scyIsInB1cmwiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjQmdXBzdHJlYW09YXBrLXRvb2xzJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyLjEyLjctcjMifSx7ImNwZXMiOlsiY3BlOjIuMzphOmJ1c3lib3g6YnVzeWJveDoxLjM1LjA6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiJmMjEzMmU4ZDZjZmUwMDZhIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlNpemUgb3B0aW1pemVkIHRvb2xib3ggb2YgbWFueSBjb21tb24gVU5JWCB1dGlsaXRpZXMiLCJmaWxlcyI6W3sicGF0aCI6Ii9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMThHOVhlTkdBVUE0M3ZpVUtzbG1kaW4yekQyOD0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9iaW4vYnVzeWJveCIsInBlcm1pc3Npb25zIjoiNzU1In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFwY2ZUZkRORWJOS1FjMnMxdGlhN2RhMDVNOFE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvYmluL3NoIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvZXRjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFtQjk1SHEyTlVUWjU5OVJEaVNzajl3NUZyT1U9In0sInBhdGgiOiIvZXRjL3NlY3VyZXR0eSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExRWdMRmpqNjdvdTNlTXFwNG0zcjJaam5RN1FVPSJ9LCJwYXRoIjoiL2V0Yy91ZGhjcGQuY29uZiJ9LHsicGF0aCI6Ii9ldGMvbG9ncm90YXRlLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVR5bHlDSU5WbW5TK0EvVGVhZDR2WmhFN0Jrcz0ifSwicGF0aCI6Ii9ldGMvbG9ncm90YXRlLmQvYWNwaWQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtdXAuZCJ9LHsicGF0aCI6Ii9ldGMvbmV0d29yay9pZi1wcmUtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU9SZitsUFJLdVlnZGtCQmNLb2V2UjF0NjBRND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvbmV0d29yay9pZi11cC5kL2RhZCIsInBlcm1pc3Npb25zIjoiNzc1In0seyJwYXRoIjoiL3NiaW4ifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdG1wIiwicGVybWlzc2lvbnMiOiIxNzc3In0seyJwYXRoIjoiL3VzciJ9LHsicGF0aCI6Ii91c3Ivc2JpbiJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUifSx7InBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExdDl2aXIvWnJYM25iU0lZVDlCRExXWmVua1ZRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS91ZGhjcGMvZGVmYXVsdC5zY3JpcHQiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii92YXIifSx7InBhdGgiOiIvdmFyL2NhY2hlIn0seyJwYXRoIjoiL3Zhci9jYWNoZS9taXNjIn0seyJwYXRoIjoiL3Zhci9saWIifSx7InBhdGgiOiIvdmFyL2xpYi91ZGhjcGQifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImExNjA1OThkNjJhMGFjNTU4NTFhYWQxOTI1MWMwMWExYmI1ZmIyMmMiLCJpbnN0YWxsZWRTaXplIjo5NDYxNzYsImxpY2Vuc2UiOiJHUEwtMi4wLW9ubHkiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSA8bmNvcGFAYWxwaW5lbGludXgub3JnPiIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6ImJ1c3lib3giLCJwdWxsQ2hlY2tzdW0iOiJRMUg2YXBoZGhZWjl1c1J2bVZqOVV0NVhRb2g5OD0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6NTAwNjA2LCJ1cmwiOiJodHRwczovL2J1c3lib3gubmV0LyIsInZlcnNpb24iOiIxLjM1LjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYnVzeWJveCIsInB1cmwiOiJwa2c6YWxwaW5lL2J1c3lib3hAMS4zNS4wP2FyY2g9eDg2XzY0JnVwc3RyZWFtPWJ1c3lib3gmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMzUuMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXM6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpjYTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2E6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjhjZWIyN2ExMmMwYmZlN2IiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk1QTC0yLjAiLCJBTkQiLCJNSVQiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlByZSBnZW5lcmF0ZWQgYnVuZGxlIG9mIE1vemlsbGEgY2VydGlmaWNhdGVzIiwiZmlsZXMiOlt7InBhdGgiOiIvZXRjIn0seyJwYXRoIjoiL2V0Yy9zc2wifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU5qNmdUQmRrWnBURlcvb2JKR2RwZnZLMFN0QT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsL2NlcnQucGVtIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvZXRjL3NzbC9jZXJ0cyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExbTJjSm9mb05adENDSzQxeXZqVGdYNEs3ZHZzPSJ9LCJwYXRoIjoiL2V0Yy9zc2wvY2VydHMvY2EtY2VydGlmaWNhdGVzLmNydCJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiNzA5YjcwYmNiNzI3MzhjZmVkYzUxMGJiYTA4MTQxYjAxMjAzODE2NyIsImluc3RhbGxlZFNpemUiOjIyMTE4NCwibGljZW5zZSI6Ik1QTC0yLjAgQU5EIE1JVCIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6ImNhLWNlcnRpZmljYXRlcyIsInBhY2thZ2UiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHVsbENoZWNrc3VtIjoiUTFTVkFXdVdIZFBIdmJCaExUa0FaNjAvMVdzbUk9IiwicHVsbERlcGVuZGVuY2llcyI6IiIsInNpemUiOjExOTc0OCwidXJsIjoiaHR0cHM6Ly93d3cubW96aWxsYS5vcmcvZW4tVVMvYWJvdXQvZ292ZXJuYW5jZS9wb2xpY2llcy9zZWN1cml0eS1ncm91cC9jZXJ0cy8iLCJ2ZXJzaW9uIjoiMjAyMTEyMjAtcjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSIsInB1cmwiOiJwa2c6YWxwaW5lL2NhLWNlcnRpZmljYXRlcy1idW5kbGVAMjAyMTEyMjAtcjA/YXJjaD14ODZfNjQmdXBzdHJlYW09Y2EtY2VydGlmaWNhdGVzJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiMTIzN2UwYzMxNWYyNjkwMiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiQlNELTItQ2xhdXNlIiwiQU5EIiwiQlNELTMtQ2xhdXNlIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJNZXRhIHBhY2thZ2UgdG8gcHVsbCBpbiBjb3JyZWN0IGxpYmMiLCJmaWxlcyI6W10sImdpdENvbW1pdE9mQXBrUG9ydCI6IjYwNDI0MTMzYmUyZTc5YmJmZWZmM2Q1ODE0N2EyMjg4NmY4MTdjZTIiLCJpbnN0YWxsZWRTaXplIjo0MDk2LCJsaWNlbnNlIjoiQlNELTItQ2xhdXNlIEFORCBCU0QtMy1DbGF1c2UiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSA8bmNvcGFAYWxwaW5lbGludXgub3JnPiIsIm9yaWdpblBhY2thZ2UiOiJsaWJjLWRldiIsInBhY2thZ2UiOiJsaWJjLXV0aWxzIiwicHVsbENoZWNrc3VtIjoiUTFlWTNqNjdWL1BpajBDQWdIUnBOZklUb0pseUk9IiwicHVsbERlcGVuZGVuY2llcyI6Im11c2wtdXRpbHMiLCJzaXplIjoxNDg1LCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZyIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJsaWJjLXV0aWxzIiwicHVybCI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1saWJjLWRldiZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMC43LjItcjMifSx7ImNwZXMiOlsiY3BlOjIuMzphOmxpYmNyeXB0bzEuMTpsaWJjcnlwdG8xLjE6MS4xLjFuLXIwOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiN2EyY2Y3MjdjYmFiODA3NCIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiT3BlblNTTCJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiQ3J5cHRvIGxpYnJhcnkgZnJvbSBvcGVuc3NsIiwiZmlsZXMiOlt7InBhdGgiOiIvZXRjIn0seyJwYXRoIjoiL2V0Yy9zc2wxLjEifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU9lVXlPRFlXZTJoQndCbTBxd3Myb0RXL1dRYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsMS4xL2NlcnQucGVtIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU5vQkY3Uk1JaVQ5ZkNYTGovbWJEaCtwbkw5bz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsMS4xL2NlcnRzIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWE3VlBSODV3cnVYRm1ORlpFL0RCYTBQeXpxMD0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsMS4xL2N0X2xvZ19saXN0LmNuZiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFvbGg4VHBkQWkyUW5UbDRGSzNUamRVaVN3VG89In0sInBhdGgiOiIvZXRjL3NzbDEuMS9jdF9sb2dfbGlzdC5jbmYuZGlzdCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExd0d1eFZFT0s5aUdMajFpOEQzQlNCblQ3TUpBPSJ9LCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXdHdXhWRU9LOWlHTGoxaThEM0JTQm5UN01KQT0ifSwicGF0aCI6Ii9ldGMvc3NsMS4xL29wZW5zc2wuY25mLmRpc3QifSx7InBhdGgiOiIvZXRjL3NzbDEuMS9wcml2YXRlIn0seyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMDBock9ZcmNXekN2MEtHOEhuUndwSE9URnNFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJjcnlwdG8uc28uMS4xIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9saWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVQyc2krYzd0czdzZ0R4UVl2ZTRCM2kxRGdvMD0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2xpYmNyeXB0by5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFMcG8yL1JMVGY1VmxIbVVRWHl2NWMxUE9wR1k9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9hZmFsZy5zbyIsInBlcm1pc3Npb25zIjoiNzU1In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsbFBxSE91M3llQVRKaFZtYXgzYUpWOXc0OHc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9jYXBpLnNvIiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMS9HaGpFUTE3S1NWYmd5cEQwNlU1VTJzd2tzWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xL3BhZGxvY2suc28iLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyIsImluc3RhbGxlZFNpemUiOjI3NDAyMjQsImxpY2Vuc2UiOiJPcGVuU1NMIiwibWFpbnRhaW5lciI6IlRpbW8gVGVyYXMgPHRpbW8udGVyYXNAaWtpLmZpPiIsIm9yaWdpblBhY2thZ2UiOiJvcGVuc3NsIiwicGFja2FnZSI6ImxpYmNyeXB0bzEuMSIsInB1bGxDaGVja3N1bSI6IlExckFzTGNiWTk2VCtUcW91ME1IMHlQUTExaEdRPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjoxMjA4MjI4LCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8iLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYmNyeXB0bzEuMSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjQmdXBzdHJlYW09b3BlbnNzbCZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0seyJjcGVzIjpbImNwZToyLjM6YTpsaWJyZXRsczpsaWJyZXRsczozLjMuNC1yMzoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjkxZGIxNWQ4MDRmZWRlNTkiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIklTQyIsIkFORCIsIihCU0QtMy1DbGF1c2UiLCJPUiIsIk1JVCkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6InBvcnQgb2YgbGlidGxzIGZyb20gbGlicmVzc2wgdG8gb3BlbnNzbCIsImZpbGVzIjpbeyJwYXRoIjoiL3VzciJ9LHsicGF0aCI6Ii91c3IvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFuTkVDOVQvdDZXK0VjbTBEeHFNVW5SdmNUNms9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9saWJ0bHMuc28uMiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFlU1dubXVzU2NsNndHY2t0a3cyLzhjcjl6RkU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9saWJ0bHMuc28uMi4wLjMiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiOTFjN2E5ZjNhYTI5NmI2ZDQ2MmM1NjM0ZTc2NThlYmRiZmY2NWJiOSIsImluc3RhbGxlZFNpemUiOjg2MDE2LCJsaWNlbnNlIjoiSVNDIEFORCAoQlNELTMtQ2xhdXNlIE9SIE1JVCkiLCJtYWludGFpbmVyIjoiQXJpYWRuZSBDb25pbGwgPGFyaWFkbmVAZGVyZWZlcmVuY2VkLm9yZz4iLCJvcmlnaW5QYWNrYWdlIjoibGlicmV0bHMiLCJwYWNrYWdlIjoibGlicmV0bHMiLCJwdWxsQ2hlY2tzdW0iOiJRMVo5L3Y1VVZzUlJrcllOZHEzcGpGQWJDdWdVOD0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIiwic2l6ZSI6MjkxODUsInVybCI6Imh0dHBzOi8vZ2l0LmNhdXNhbC5hZ2VuY3kvbGlicmV0bHMvIiwidmVyc2lvbiI6IjMuMy40LXIzIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYnJldGxzIiwicHVybCI6InBrZzphbHBpbmUvbGlicmV0bHNAMy4zLjQtcjM/YXJjaD14ODZfNjQmdXBzdHJlYW09bGlicmV0bHMmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjMuMy40LXIzIn0seyJjcGVzIjpbImNwZToyLjM6YTpsaWJzc2wxLjE6bGlic3NsMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjMwOTRjNGE2MTBiMGIwMGQiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk9wZW5TU0wiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlNTTCBzaGFyZWQgbGlicmFyaWVzIiwiZmlsZXMiOlt7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF4TmpqN2p4dk9qM2xEUmQzc1JYekhvd1RVc1E9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xpYnNzbC5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExOGozNXBlM3lwNkhPZ01paDF3bEdQMS9tbTJjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlic3NsLnNvLjEuMSIsInBlcm1pc3Npb25zIjoiNzc3In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiI0NTVlOTY2ODk5YTkzNThmYzk0ZjViY2U2MzNhZmU4YTE5NDIwOTVjIiwiaW5zdGFsbGVkU2l6ZSI6NTQwNjcyLCJsaWNlbnNlIjoiT3BlblNTTCIsIm1haW50YWluZXIiOiJUaW1vIFRlcmFzIDx0aW1vLnRlcmFzQGlraS5maT4iLCJvcmlnaW5QYWNrYWdlIjoib3BlbnNzbCIsInBhY2thZ2UiOiJsaWJzc2wxLjEiLCJwdWxsQ2hlY2tzdW0iOiJRMS9LWjAwcURIV1o1Y2ozQVdHL0RQZEFDUk5ZST0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEiLCJzaXplIjoyMTMyMDksInVybCI6Imh0dHBzOi8vd3d3Lm9wZW5zc2wub3JnLyIsInZlcnNpb24iOiIxLjEuMW4tcjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibGlic3NsMS4xIiwicHVybCI6InBrZzphbHBpbmUvbGlic3NsMS4xQDEuMS4xbi1yMD9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1vcGVuc3NsJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImNwZXMiOlsiY3BlOjIuMzphOm11c2w6bXVzbDoxLjIuMi1yNzoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjRhYzcxMzZiODUzNmNkZWEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk1JVCJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImZpbGVzIjpbeyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMmFkd3FRT2pvOWRGbCtWSkQyRWNkOTAxdmhFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9sZC1tdXNsLXg4Nl82NC5zby4xIiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTd5SjNKRk55cEE0bXhoSkpyMG91NkN6c0pWST0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliYy5tdXNsLXg4Nl82NC5zby4xIiwicGVybWlzc2lvbnMiOiI3NzcifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImJmNWJiZmRiZjc4MDA5MmYzODdiN2FiZTQwMWZiZmNlZGE5MGM4NGQiLCJpbnN0YWxsZWRTaXplIjo2MjI1OTIsImxpY2Vuc2UiOiJNSVQiLCJtYWludGFpbmVyIjoiVGltbyBUZXLDpHMgPHRpbW8udGVyYXNAaWtpLmZpPiIsIm9yaWdpblBhY2thZ2UiOiJtdXNsIiwicGFja2FnZSI6Im11c2wiLCJwdWxsQ2hlY2tzdW0iOiJRMURlYjBqTnl0a3JqUFc0Ti9lS0xaNDNCd09sdz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiIiwic2l6ZSI6MzgzMTUyLCJ1cmwiOiJodHRwczovL211c2wubGliYy5vcmcvIiwidmVyc2lvbiI6IjEuMi4yLXI3In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6Im11c2wiLCJwdXJsIjoicGtnOmFscGluZS9tdXNsQDEuMi4yLXI3P2FyY2g9eDg2XzY0JnVwc3RyZWFtPW11c2wmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMi4yLXI3In0seyJjcGVzIjpbImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk1JVCIsIkJTRCIsIkdQTDIrIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJ0aGUgbXVzbCBjIGxpYnJhcnkgKGxpYmMpIGltcGxlbWVudGF0aW9uIiwiZmlsZXMiOlt7InBhdGgiOiIvc2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExS2phMitQT1pLeEVrVU9acXdTakM2a21hRUQ0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3NiaW4vbGRjb25maWciLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTWkyMUJUY0x0TjljWVBWMDdQMGF3SHlUNlhVPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0Y29uZiIsInBlcm1pc3Npb25zIjoiNzU1In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFuWm1ES0tGUTJ2b29JdE5ETEJsZVQ4eDdPTUE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2Jpbi9nZXRlbnQiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExUThUT09kNVRtMlB0a081RW9vd3ZodkdDSUo0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vaWNvbnYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExeUZBaEdnZ21MN0VSZ2JJQTdLUXh5VHpmM2tzPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vbGRkIiwicGVybWlzc2lvbnMiOiI3NTUifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImJmNWJiZmRiZjc4MDA5MmYzODdiN2FiZTQwMWZiZmNlZGE5MGM4NGQiLCJpbnN0YWxsZWRTaXplIjoxNDMzNjAsImxpY2Vuc2UiOiJNSVQgQlNEIEdQTDIrIiwibWFpbnRhaW5lciI6IlRpbW8gVGVyw6RzIDx0aW1vLnRlcmFzQGlraS5maT4iLCJvcmlnaW5QYWNrYWdlIjoibXVzbCIsInBhY2thZ2UiOiJtdXNsLXV0aWxzIiwicHVsbENoZWNrc3VtIjoiUTFQNTBjZkppU3NIb3FzWVJUeU9FT2xKaUxuM289IiwicHVsbERlcGVuZGVuY2llcyI6InNjYW5lbGYgc286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6MzY3MjMsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8iLCJ2ZXJzaW9uIjoiMS4yLjItcjcifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibXVzbC11dGlscyIsInB1cmwiOiJwa2c6YWxwaW5lL211c2wtdXRpbHNAMS4yLjItcjc/YXJjaD14ODZfNjQmdXBzdHJlYW09bXVzbCZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4yLjItcjcifSx7ImNwZXMiOlsiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjFmMjhkZTEyMDA3M2Q3OGEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiU2NhbiBFTEYgYmluYXJpZXMgZm9yIHN0dWZmIiwiZmlsZXMiOlt7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNGZTU0UmJkZlQ0Q05pbVltNDFEMUR2K05zZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL3NjYW5lbGYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyIsImluc3RhbGxlZFNpemUiOjk0MjA4LCJsaWNlbnNlIjoiR1BMLTIuMC1vbmx5IiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgPG5jb3BhQGFscGluZWxpbnV4Lm9yZz4iLCJvcmlnaW5QYWNrYWdlIjoicGF4LXV0aWxzIiwicGFja2FnZSI6InNjYW5lbGYiLCJwdWxsQ2hlY2tzdW0iOiJRMTEvZFpEa1VJY0tUM2xuSENOcHN4dGJzSE5Kbz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6MzY4MzAsInVybCI6Imh0dHBzOi8vd2lraS5nZW50b28ub3JnL3dpa2kvSGFyZGVuZWQvUGFYX1V0aWxpdGllcyIsInZlcnNpb24iOiIxLjMuMy1yMCJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJzY2FuZWxmIiwicHVybCI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1wYXgtdXRpbHMmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMy4zLXIwIn0seyJjcGVzIjpbImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbC1jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsLWNsaWVudDpzc2xfY2xpZW50OjEuMzUuMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsLWNsaWVudDoxLjM1LjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2xfY2xpZW50OnNzbF9jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsOnNzbC1jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsOnNzbF9jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiYzJlM2NlN2I5ZTcyZDBhZSIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiR1BMLTIuMC1vbmx5Il0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJFWHRlcm5hbCBzc2xfY2xpZW50IGZvciBidXN5Ym94IHdnZXQiLCJmaWxlcyI6W3sicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExNXVlRi9QL0VtQVZWbVVKMVBYQkFrcENBWDdjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vc3NsX2NsaWVudCIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiJhMTYwNTk4ZDYyYTBhYzU1ODUxYWFkMTkyNTFjMDFhMWJiNWZiMjJjIiwiaW5zdGFsbGVkU2l6ZSI6Mjg2NzIsImxpY2Vuc2UiOiJHUEwtMi4wLW9ubHkiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSA8bmNvcGFAYWxwaW5lbGludXgub3JnPiIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6InNzbF9jbGllbnQiLCJwdWxsQ2hlY2tzdW0iOiJRMTNHdzVIRWVMWmorUWhqN0hSbXd6aTBVT0Fndz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYnRscy5zby4yIiwic2l6ZSI6NDcwOSwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8iLCJ2ZXJzaW9uIjoiMS4zNS4wIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6InNzbF9jbGllbnQiLCJwdXJsIjoicGtnOmFscGluZS9zc2xfY2xpZW50QDEuMzUuMD9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1idXN5Ym94JmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjM1LjAifSx7ImNwZXMiOlsiY3BlOjIuMzphOnpsaWI6emxpYjoxLjIuMTEtcjM6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiJiZTA2ZGY2ZTNiYmJmMGFiIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJabGliIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJBIGNvbXByZXNzaW9uL2RlY29tcHJlc3Npb24gTGlicmFyeSIsImZpbGVzIjpbeyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExYTJIOGhQMjRyeUNBR2Y4Zm5jMU5oYTlJSUhjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJ6LnNvLjEiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTTRPSExVWWh2SG9scWdNQjBUYzNrRWxLN3FBPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJ6LnNvLjEuMi4xMSIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiIzODhhNGZiMzY0MGY4Y2NiZDE4ZTEwNWRmM2FkNzQxZGNhNDI0N2UxLWRpcnR5IiwiaW5zdGFsbGVkU2l6ZSI6MTEwNTkyLCJsaWNlbnNlIjoiWmxpYiIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6InpsaWIiLCJwYWNrYWdlIjoiemxpYiIsInB1bGxDaGVja3N1bSI6IlExV0JvKzU3SmxkbFZlMGlWdDJuOElQNit2TkdFPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjo1MTc0MiwidXJsIjoiaHR0cHM6Ly96bGliLm5ldC8iLCJ2ZXJzaW9uIjoiMS4yLjExLXIzIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6InpsaWIiLCJwdXJsIjoicGtnOmFscGluZS96bGliQDEuMi4xMS1yMz9hcmNoPXg4Nl82NCZ1cHN0cmVhbT16bGliJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjIuMTEtcjMifV0sImRlc2NyaXB0b3IiOnsiY29uZmlndXJhdGlvbiI6eyJhbmNob3JlIjp7ImRvY2tlcmZpbGUiOiIiLCJob3N0IjoiIiwiaW1wb3J0LXRpbWVvdXQiOjMwLCJvdmVyd3JpdGUtZXhpc3RpbmctaW1hZ2UiOmZhbHNlLCJwYXRoIjoiIn0sImF0dGVzdCI6eyJrZXkiOiJjb3NpZ24ua2V5In0sImNoZWNrLWZvci1hcHAtdXBkYXRlIjp0cnVlLCJjb25maWdQYXRoIjoiIiwiZGV2Ijp7InByb2ZpbGUtY3B1IjpmYWxzZSwicHJvZmlsZS1tZW0iOmZhbHNlfSwiZXhjbHVkZSI6W10sImZpbGUiOiIiLCJmaWxlLWNsYXNzaWZpY2F0aW9uIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9fSwiZmlsZS1jb250ZW50cyI6eyJjYXRhbG9nZXIiOnsiZW5hYmxlZCI6ZmFsc2UsInNjb3BlIjoiU3F1YXNoZWQifSwiZ2xvYnMiOltdLCJza2lwLWZpbGVzLWFib3ZlLXNpemUiOjEwNDg1NzZ9LCJmaWxlLW1ldGFkYXRhIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9LCJkaWdlc3RzIjpbInNoYTI1NiJdfSwibG9nIjp7ImZpbGUtbG9jYXRpb24iOiIiLCJsZXZlbCI6ImVycm9yIiwic3RydWN0dXJlZCI6ZmFsc2V9LCJvdXRwdXQiOlsianNvbiJdLCJwYWNrYWdlIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjp0cnVlLCJzY29wZSI6IlNxdWFzaGVkIn0sInNlYXJjaC1pbmRleGVkLWFyY2hpdmVzIjp0cnVlLCJzZWFyY2gtdW5pbmRleGVkLWFyY2hpdmVzIjpmYWxzZX0sInBsYXRmb3JtIjoiIiwicXVpZXQiOmZhbHNlLCJyZWdpc3RyeSI6eyJhdXRoIjpbXSwiaW5zZWN1cmUtc2tpcC10bHMtdmVyaWZ5IjpmYWxzZSwiaW5zZWN1cmUtdXNlLWh0dHAiOmZhbHNlfSwic2VjcmV0cyI6eyJhZGRpdGlvbmFsLXBhdHRlcm5zIjp7fSwiY2F0YWxvZ2VyIjp7ImVuYWJsZWQiOmZhbHNlLCJzY29wZSI6IkFsbExheWVycyJ9LCJleGNsdWRlLXBhdHRlcm4tbmFtZXMiOltdLCJyZXZlYWwtdmFsdWVzIjpmYWxzZSwic2tpcC1maWxlcy1hYm92ZS1zaXplIjoxMDQ4NTc2fX0sIm5hbWUiOiJzeWZ0IiwidmVyc2lvbiI6IjAuNDIuMSJ9LCJkaXN0cm8iOnsiYnVnUmVwb3J0VVJMIjoiaHR0cHM6Ly9idWdzLmFscGluZWxpbnV4Lm9yZy8iLCJob21lVVJMIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcvIiwiaWQiOiJhbHBpbmUiLCJuYW1lIjoiQWxwaW5lIExpbnV4IiwicHJldHR5TmFtZSI6IkFscGluZSBMaW51eCB2My4xNSIsInZlcnNpb25JRCI6IjMuMTUuMiJ9LCJmaWxlcyI6W3siaWQiOiI2YzczMTQ0ZWE5ZWY0ZmI5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9iaW4vYnVzeWJveCJ9fSx7ImlkIjoiZTQxZGFkOGE2MWZmODNiNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiYzliZWM1M2MwYTNlMjEwNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYTVkZmQyOTE0ODFjNjllMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiYzE1ZjkyZGRjNzdjMWFlNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiZDBjMjQ1M2Y5OGIzMTFkOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QifX0seyJpZCI6ImM3OTVkM2Y3ZWJkNjRlODQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9mc3RhYiJ9fSx7ImlkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2dyb3VwIn19LHsiaWQiOiJkZjIyZWQxYTMxZmRiYjY4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvaG9zdG5hbWUifX0seyJpZCI6ImU4NWFmMTRkNTAzMjU3NDQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9ob3N0cyJ9fSx7ImlkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2luaXR0YWIifX0seyJpZCI6IjlmYTk0ZWE4ZTg3NGVlNDkiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZC9hY3BpZCJ9fSx7ImlkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn19LHsiaWQiOiJkYmU3MTZlMDVmOTExZmY5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9ibGFja2xpc3QuY29uZiJ9fSx7ImlkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvaTM4Ni5jb25mIn19LHsiaWQiOiI5YTM1NzZlNGFhZGI4MmYxIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9fSx7ImlkIjoiNWEyNWQwZGJlODBhYjI0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9fSx7ImlkIjoiNTU1OGY3NjAzOGQxYzk3ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vdGQifX0seyJpZCI6IjU1OWE5ZDUxZTMzZDI0MTAiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQvZGFkIn19LHsiaWQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcGFzc3dkIn19LHsiaWQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9fSx7ImlkIjoiMzg3MTJhZGVhZWRiNzMxNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9SRUFETUUifX0seyJpZCI6IjE0ZGQ5ODVkY2U0Y2Q1YTUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvY29sb3JfcHJvbXB0LnNoLmRpc2FibGVkIn19LHsiaWQiOiI1OTBjYzI2MzU4N2UyYzlkIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9fSx7ImlkIjoiMjZhMjBmMTA2ZmEzMjc4MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb3RvY29scyJ9fSx7ImlkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlY3VyZXR0eSJ9fSx7ImlkIjoiZjI5YWYyNDc1NTBkY2FjOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlcnZpY2VzIn19LHsiaWQiOiI0YTZkNmUwZWE5NTc1MWI3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hhZG93In19LHsiaWQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hlbGxzIn19LHsiaWQiOiI1YTg0NzZiZDZmNWExM2JmIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc3NsL2NlcnRzL2NhLWNlcnRpZmljYXRlcy5jcnQifX0seyJpZCI6ImJjNjYzNjJiMjVjNjQzNGEiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY3RfbG9nX2xpc3QuY25mLmRpc3QifX0seyJpZCI6ImRjNWNiYjc4Y2M3NDBmYmUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYifX0seyJpZCI6ImRlYzIzOWI1NjVkMTliNjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYuZGlzdCJ9fSx7ImlkIjoiYjhmMWU0YTYyYjIxNjYzYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3N5c2N0bC5jb25mIn19LHsiaWQiOiI2YzZmOThjODU3YTY5MWNjIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvdWRoY3BkLmNvbmYifX0seyJpZCI6IjRhY2E2ZDE1ZGYwOWRhMWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9sZC1tdXNsLXg4Nl82NC5zby4xIn19LHsiaWQiOiI4NTI0ZGNmMDI5MzZkODE3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliYXBrLnNvLjMuMTIuMCJ9fSx7ImlkIjoiYmExYWQxNTM5NGM1MzkyMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2xpYmNyeXB0by5zby4xLjEifX0seyJpZCI6IjZiMWM5YTE4Y2FiYWYzZWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9saWJzc2wuc28uMS4xIn19LHsiaWQiOiIxM2ExYWJkMWQyZDM0YTU3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliei5zby4xLjIuMTEifX0seyJpZCI6IjQ1NmE3MDJmMTY3MzNiMjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9zeXNjdGwuZC8wMC1hbHBpbmUuY29uZiJ9fSx7ImlkIjoiMmM2MjRmYmYzOWNhN2Q2NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvc2Jpbi9hcGsifX0seyJpZCI6IjIzNzAyNmQxMzRiODRkNmYiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbGRjb25maWcifX0seyJpZCI6IjE0ZTNmNDFhNzdiNDI4MDciLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbWttbnRkaXJzIn19LHsiaWQiOiIzY2E3Y2JmZDU5MzBkOTBiIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2dldGNvbmYifX0seyJpZCI6IjlhY2M0NmY5ZjlkMjQwNTgiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0ZW50In19LHsiaWQiOiIxNjJmMzI0MDM2NTYxNjQzIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2ljb252In19LHsiaWQiOiJhMjliNDViNWMwMmQwMDA3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2xkZCJ9fSx7ImlkIjoiMmFmNDEyYjEwZmYyYzVmYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2Jpbi9zY2FuZWxmIn19LHsiaWQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL3NzbF9jbGllbnQifX0seyJpZCI6ImU2MDU0MmZhMzNkYWViMDUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvYWZhbGcuc28ifX0seyJpZCI6IjJkMjRjNDBhODhiNGMyZWMiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvY2FwaS5zbyJ9fSx7ImlkIjoiNWRiMzhkZGNlODkxMDVlNyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9wYWRsb2NrLnNvIn19LHsiaWQiOiIyYjAyZjY3OTBjNmVmN2U4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvbGliL2xpYnRscy5zby4yLjAuMyJ9fSx7ImlkIjoiYTJiZjQxNDAyY2MwNDMxNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiZGI0NjJiZDRjZTliYWVjNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYzVmMTI2MmE2NmJmMDQ4YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiJ9fSx7ImlkIjoiZWUzYWQ5YzBhNGY1NGU5MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiOTJkMzU4ZTcyNTZmMGI3YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTgxOTlkY2MucnNhLnB1YiJ9fSx7ImlkIjoiNThmYmViYTc3NGViMmE1NiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThjYmI0NzYucnNhLnB1YiJ9fSx7ImlkIjoiZmM4MjgxZjNiZmQ1NzFlNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiJ9fSx7ImlkIjoiZGMxZTA0M2M2NTFkZmI0YyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNWU2OWNhNTAucnNhLnB1YiJ9fSx7ImlkIjoiNzZiNWFhODA1ZGMxYjE4ZiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiJ9fSx7ImlkIjoiYTAwZjcxNjgxOWZhYTgxOSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiYTNlNzZmMjE5MzNiNTFjNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMTFkNGU3NjhjMmViNzYyZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YTk3MjQucnNhLnB1YiJ9fSx7ImlkIjoiNTIyYTYwMmU5YzEwMGU2MiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiJ9fSx7ImlkIjoiYWM3YTBmMjFkZWMwM2ExYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWMzYmMucnNhLnB1YiJ9fSx7ImlkIjoiMzkxMTQ1ZjBlZmQzOWI5NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiJ9fSx7ImlkIjoiMjk1YzI2M2IzZDFjODFkMiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiJ9fSx7ImlkIjoiNGE5NmY4YzRjZjM0Njc1OSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2ZGIzMGQucnNhLnB1YiJ9fSx7ImlkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYy9kZWZhdWx0LnNjcmlwdCJ9fV0sInNjaGVtYSI6eyJ1cmwiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYW5jaG9yZS9zeWZ0L21haW4vc2NoZW1hL2pzb24vc2NoZW1hLTMuMi4xLmpzb24iLCJ2ZXJzaW9uIjoiMy4yLjEifSwic291cmNlIjp7InRhcmdldCI6eyJhcmNoaXRlY3R1cmUiOiIiLCJjb25maWciOiJleUpoY21Ob2FYUmxZM1IxY21VaU9pSmhiV1EyTkNJc0ltTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJaUlzSWtSdmJXRnBibTVoYldVaU9pSWlMQ0pWYzJWeUlqb2lJaXdpUVhSMFlXTm9VM1JrYVc0aU9tWmhiSE5sTENKQmRIUmhZMmhUZEdSdmRYUWlPbVpoYkhObExDSkJkSFJoWTJoVGRHUmxjbklpT21aaGJITmxMQ0pVZEhraU9tWmhiSE5sTENKUGNHVnVVM1JrYVc0aU9tWmhiSE5sTENKVGRHUnBiazl1WTJVaU9tWmhiSE5sTENKRmJuWWlPbHNpVUVGVVNEMHZkWE55TDJ4dlkyRnNMM05pYVc0NkwzVnpjaTlzYjJOaGJDOWlhVzQ2TDNWemNpOXpZbWx1T2k5MWMzSXZZbWx1T2k5elltbHVPaTlpYVc0aVhTd2lRMjFrSWpwYklpOWlhVzR2YzJnaVhTd2lTVzFoWjJVaU9pSnphR0V5TlRZNlpUaGxNRE5oWldVMlpqRTJaRGhsWW1GbU9UVmhORGs0TXpBd01UTTNNMkkwTkRjNU1HTmpPR1EwTW1NMVpUZzBZV1kwWmpObU9ERTBaVFF4TWpFeVppSXNJbFp2YkhWdFpYTWlPbTUxYkd3c0lsZHZjbXRwYm1kRWFYSWlPaUlpTENKRmJuUnllWEJ2YVc1MElqcHVkV3hzTENKUGJrSjFhV3hrSWpwdWRXeHNMQ0pNWVdKbGJITWlPbTUxYkd4OUxDSmpiMjUwWVdsdVpYSWlPaUprTXpJMk1qQXlOR0ZqTUdZd09EYzVNR1ppTWpNMlpXRXpNbU0xT1dGak1XWmtNamMzTldVeU1qYzFNR1UxWXpCaVlXWTFaV0UxWXpBeE1qTXhOR0ppSWl3aVkyOXVkR0ZwYm1WeVgyTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJbVF6TWpZeU1ESTBZV013WmlJc0lrUnZiV0ZwYm01aGJXVWlPaUlpTENKVmMyVnlJam9pSWl3aVFYUjBZV05vVTNSa2FXNGlPbVpoYkhObExDSkJkSFJoWTJoVGRHUnZkWFFpT21aaGJITmxMQ0pCZEhSaFkyaFRkR1JsY25JaU9tWmhiSE5sTENKVWRIa2lPbVpoYkhObExDSlBjR1Z1VTNSa2FXNGlPbVpoYkhObExDSlRkR1JwYms5dVkyVWlPbVpoYkhObExDSkZibllpT2xzaVVFRlVTRDB2ZFhOeUwyeHZZMkZzTDNOaWFXNDZMM1Z6Y2k5c2IyTmhiQzlpYVc0NkwzVnpjaTl6WW1sdU9pOTFjM0l2WW1sdU9pOXpZbWx1T2k5aWFXNGlYU3dpUTIxa0lqcGJJaTlpYVc0dmMyZ2lMQ0l0WXlJc0lpTW9ibTl3S1NBaUxDSkRUVVFnVzF3aUwySnBiaTl6YUZ3aVhTSmRMQ0pKYldGblpTSTZJbk5vWVRJMU5qcGxPR1V3TTJGbFpUWm1NVFprT0dWaVlXWTVOV0UwT1Rnek1EQXhNemN6WWpRME56a3dZMk00WkRReVl6VmxPRFJoWmpSbU0yWTRNVFJsTkRFeU1USm1JaXdpVm05c2RXMWxjeUk2Ym5Wc2JDd2lWMjl5YTJsdVowUnBjaUk2SWlJc0lrVnVkSEo1Y0c5cGJuUWlPbTUxYkd3c0lrOXVRblZwYkdRaU9tNTFiR3dzSWt4aFltVnNjeUk2ZTMxOUxDSmpjbVZoZEdWa0lqb2lNakF5TWkwd015MHlNMVF4TlRveU1Ub3lNUzR4TVRrNU1EY3pNVGxhSWl3aVpHOWphMlZ5WDNabGNuTnBiMjRpT2lJeU1DNHhNQzR4TWlJc0ltaHBjM1J2Y25raU9sdDdJbU55WldGMFpXUWlPaUl5TURJeUxUQXpMVEl6VkRFMU9qSXhPakl4TGpBeU16SXpOVGM0TmxvaUxDSmpjbVZoZEdWa1gySjVJam9pTDJKcGJpOXphQ0F0WXlBaktHNXZjQ2tnUVVSRUlHWnBiR1U2TnpNNE5tRmtPRGt6TmpjeU1EQTNZMk5oTW1RM00yTmxZekU0TmpKa05UZ3lZVFk1WkRVNE1XTmhNV1F4TlRWa05EVTVPV05pTW1GaE5UUmtOVFE1T0NCcGJpQXZJQ0o5TEhzaVkzSmxZWFJsWkNJNklqSXdNakl0TURNdE1qTlVNVFU2TWpFNk1qRXVNVEU1T1RBM016RTVXaUlzSW1OeVpXRjBaV1JmWW5raU9pSXZZbWx1TDNOb0lDMWpJQ01vYm05d0tTQWdRMDFFSUZ0Y0lpOWlhVzR2YzJoY0lsMGlMQ0psYlhCMGVWOXNZWGxsY2lJNmRISjFaWDFkTENKdmN5STZJbXhwYm5WNElpd2ljbTl2ZEdaeklqcDdJblI1Y0dVaU9pSnNZWGxsY25NaUxDSmthV1ptWDJsa2N5STZXeUp6YUdFeU5UWTZabVkzTmpoaE1UUXhNMkpoTVRBNU16QTRZekJrT0RrM1ptWmhOVFUxWlRBMU1tRTBObVF5WTJZME56RXhOemhtTURBeFlqZ3lOV0kwWXpJeFpqTTFOQ0pkZlgwPSIsImltYWdlSUQiOiJzaGEyNTY6OWM4NDJhYzQ5YTM5ZmU0MmU3MWE2MjMxODNmZTdmYjdjNzU5ZDU5MDI5ZTlhOGU3ODUxYzM1N2M3ZDhhODZmOCIsImltYWdlU2l6ZSI6NTU3MDE0NywibGF5ZXJzIjpbeyJkaWdlc3QiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1NTcwMTQ3fV0sIm1hbmlmZXN0IjoiZXdvZ0lDQWljMk5vWlcxaFZtVnljMmx2YmlJNklESXNDaUFnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1a2FYTjBjbWxpZFhScGIyNHViV0Z1YVdabGMzUXVkaklyYW5OdmJpSXNDaUFnSUNKamIyNW1hV2NpT2lCN0NpQWdJQ0FnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1amIyNTBZV2x1WlhJdWFXMWhaMlV1ZGpFcmFuTnZiaUlzQ2lBZ0lDQWdJQ0p6YVhwbElqb2dNVFEzTWl3S0lDQWdJQ0FnSW1ScFoyVnpkQ0k2SUNKemFHRXlOVFk2T1dNNE5ESmhZelE1WVRNNVptVTBNbVUzTVdFMk1qTXhPRE5tWlRkbVlqZGpOelU1WkRVNU1ESTVaVGxoT0dVM09EVXhZek0xTjJNM1pEaGhPRFptT0NJS0lDQWdmU3dLSUNBZ0lteGhlV1Z5Y3lJNklGc0tJQ0FnSUNBZ2V3b2dJQ0FnSUNBZ0lDQWliV1ZrYVdGVWVYQmxJam9nSW1Gd2NHeHBZMkYwYVc5dUwzWnVaQzVrYjJOclpYSXVhVzFoWjJVdWNtOXZkR1p6TG1ScFptWXVkR0Z5TG1kNmFYQWlMQW9nSUNBZ0lDQWdJQ0FpYzJsNlpTSTZJREk0TVRJMk9Ea3NDaUFnSUNBZ0lDQWdJQ0prYVdkbGMzUWlPaUFpYzJoaE1qVTJPak5oWVRSa01HSmlaR1V4T1RKaVptRmlZVGMxWmpKa01USTBaRGhqWmpKbE5tUmxORFV5WVdVd00yVTFOV1ExTkRFd05XVTBObUl3Tm1WaU9ERXlOMlVpQ2lBZ0lDQWdJSDBLSUNBZ1hRcDkiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo3M2MxNTU2OTZmZTY1YjY4Njk2ZTZlYTI0MDg4NjkzNTQ2YWM0NjhiM2UxNDU0MmYyM2YwZWZiZGUyODljYzk3IiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsIm9zIjoiIiwicmVwb0RpZ2VzdHMiOlsiaW5kZXguZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lQHNoYTI1Njo2YWYxYjExYmJiMTdmNGMzMTFlMjY5ZGI2NTMwZTRkYTI3MzgyNjJhZjVmZDkwNjRjY2RmMTA5Yjc2NTg2MGZiIl0sInRhZ3MiOltdLCJ1c2VySW5wdXQiOiJhbHBpbmU6bGF0ZXN0In0sInR5cGUiOiJpbWFnZSJ9fX0K","signatures":[{"keyid":"","sig":"MEUCIQDBtal1MWSsNl8U1neDA1Ujec8HvbJ5T4tWtuFNY7OkrgIgFY+wklqhg6Y/HhivWlMmcA593sx6pNnusAqTLlNtIP0="}]} ================================================ FILE: grype/pkg/testdata/alpine-tampered.cdx.att.json ================================================ {"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiIiaHR0cHM6Ly9zeWZ0LmRldi9ib20iIiwic3ViamVjdCI6W3sibmFtZSI6IiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI0ZWRiZDJiZWI1Zjc4YjEwMTQwMjhmNGZiYjk5ZjMyMzdkOTU2MTEwMGI2ODgxYWFiYmY1YWNjZTJjNGY5NDU0In19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsImNvbXBvbmVudHMiOlt7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2FscGluZS1iYXNlbGF5b3V0QDMuMi4wLXIxOD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTlmNTI3MjEzZjRkMmE4NzMiLCJjcGUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0LmFscGluZWxpbnV4Lm9yZy9jZ2l0L2Fwb3J0cy90cmVlL21haW4vYWxwaW5lLWJhc2VsYXlvdXQifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNDEzNjk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUtYmFzZWxheW91dCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMUV5bVM2ckFnbUdzN1hZaHFkeUVvaVdnRVo2QT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Ii9iaW4vc2ggc286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyMTEwMSJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUtYmFzZWxheW91dEAzLjIuMC1yMTg/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUtYmFzZWxheW91dFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4yLjAtcjE4In0seyJib20tcmVmIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUta2V5c1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xYTcyY2EzYjg4ZTFiNjdlIiwiY3BlIjoiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJQdWJsaWMga2V5cyBmb3IgQWxwaW5lIExpbnV4IHBhY2thZ2VzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6ImFscGluZS1rZXlzIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfa2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYWFiNjhmOGM5YWI0MzRhNDY3MTBkZThlMTJmYjMyMDZlMjkzMGE1OSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTU5NzQ0In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUta2V5cyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWtERjJzdEtvM2UvUnVtbEE4WnJSZkN3ZFN2OD0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjEzMzYyIn1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2FscGluZS1rZXlzQDIuNC1yMT9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1rZXlzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyLjQtcjEifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MWM2ZTA1N2M2OTY1YmRkNiIsImNwZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBbHBpbmUgUGFja2FnZSBLZWVwZXIgLSBwYWNrYWdlIG1hbmFnZXIgZm9yIGFscGluZSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0bGFiLmFscGluZWxpbnV4Lm9yZy9hbHBpbmUvYXBrLXRvb2xzIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkdQTC0yLjAtb25seSJ9fV0sIm5hbWUiOiJhcGstdG9vbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwa190b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YXBrX3Rvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGs6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwazphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6bGF5ZXJJRCIsInZhbHVlIjoic2hhMjU2OjRmYzI0MmQ1ODI4NTY5OWVjYTA1ZGIzY2M3YzcxMjJhMmI4ZTAxNGQ5NDgxZjMyM2JkOTI3N2JhYWNmYTA2MjgifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6cGF0aCIsInZhbHVlIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpnaXRDb21taXRPZkFwa1BvcnQiLCJ2YWx1ZSI6IjFhYzNjMWJiMjllZWZmMDgzYzYyMWNmNmIyN2FkMTJhYjkzY2I3M2EifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmluc3RhbGxlZFNpemUiLCJ2YWx1ZSI6IjMxMTI5NiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoiYXBrLXRvb2xzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExM2ZQZCtGUlhhTHd5TmtsVm4rcXVGV0R5a25NPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoibXVzbFx1MDAzZT0xLjIgY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIHNvOmxpYnouc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwMzc3In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNCIsInR5cGUiOiJsaWJyYXJ5IiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWE1MjcwMjYxMmFhMTZkZTMiLCJjcGUiOiJjcGU6Mi4zOmE6YnVzeWJveDpidXN5Ym94OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTaXplIG9wdGltaXplZCB0b29sYm94IG9mIG1hbnkgY29tbW9uIFVOSVggdXRpbGl0aWVzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImJ1c3lib3giLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQ2MTc2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJidXN5Ym94In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExTFV5UEtJS3pVSzZ2cEpjQmorTzQwNGVmYXdnPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiI1MDA2NzgifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2NhLWNlcnRpZmljYXRlcy1idW5kbGVAMjAyMTEyMjAtcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1jYS1jZXJ0aWZpY2F0ZXNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MmM0NTIyMjkyM2QzMGZjNSIsImNwZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiUHJlIGdlbmVyYXRlZCBidW5kbGUgb2YgTW96aWxsYSBjZXJ0aWZpY2F0ZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy9hYm91dC9nb3Zlcm5hbmNlL3BvbGljaWVzL3NlY3VyaXR5LWdyb3VwL2NlcnRzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJNUEwtMi4wIn19LHsibGljZW5zZSI6eyJpZCI6Ik1JVCJ9fV0sIm5hbWUiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXM6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2E6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3MDliNzBiY2I3MjczOGNmZWRjNTEwYmJhMDgxNDFiMDEyMDM4MTY3In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIyMjExODQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImNhLWNlcnRpZmljYXRlcyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMVNWQVd1V0hkUEh2YkJoTFRrQVo2MC8xV3NtST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjExOTc0OCJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9jYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlQDIwMjExMjIwLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09Y2EtY2VydGlmaWNhdGVzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWU4N2E3OWZkYWVjYWFiZDIiLCJjcGUiOiJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6Ik1ldGEgcGFja2FnZSB0byBwdWxsIGluIGNvcnJlY3QgbGliYyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vYWxwaW5lbGludXgub3JnIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkJTRC0yLUNsYXVzZSJ9fSx7ImxpY2Vuc2UiOnsiaWQiOiJCU0QtMy1DbGF1c2UifX1dLCJuYW1lIjoibGliYy11dGlscyIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmMtdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI2MDQyNDEzM2JlMmU3OWJiZmVmZjNkNTgxNDdhMjI4ODZmODE3Y2UyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiI0MDk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJsaWJjLWRldiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWVZM2o2N1YvUGlqMENBZ0hScE5mSVRvSmx5ST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Im11c2wtdXRpbHMifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjE0ODUifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliY3J5cHRvMS4xQDEuMS4xbi1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW9wZW5zc2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MTdjNmZiYjBjYWZiYmU1NyIsImNwZSI6ImNwZToyLjM6YTpsaWJjcnlwdG8xLjE6bGliY3J5cHRvMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJDcnlwdG8gbGlicmFyeSBmcm9tIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJjcnlwdG8xLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjc0MDIyNCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoib3BlbnNzbCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMXJBc0xjYlk5NlQrVHFvdTBNSDB5UFExMWhHUT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwODIyOCJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnJldGxzQDMuMy40LXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09bGlicmV0bHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9NmQwNjU4YzJiNzIzN2VhMiIsImNwZSI6ImNwZToyLjM6YTpsaWJyZXRsczpsaWJyZXRsczozLjMuNC1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJwb3J0IG9mIGxpYnRscyBmcm9tIGxpYnJlc3NsIHRvIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL2dpdC5jYXVzYWwuYWdlbmN5L2xpYnJldGxzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJJU0MifX1dLCJuYW1lIjoibGlicmV0bHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiOTFjN2E5ZjNhYTI5NmI2ZDQ2MmM1NjM0ZTc2NThlYmRiZmY2NWJiOSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiODYwMTYifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImxpYnJldGxzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExWjkvdjVVVnNSUmtyWU5kcTNwakZBYkN1Z1U4PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyOTE4NSJ9XSwicHVibGlzaGVyIjoiQXJpYWRuZSBDb25pbGwgXHUwMDNjYXJpYWRuZUBkZXJlZmVyZW5jZWQub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGlicmV0bHNAMy4zLjQtcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1saWJyZXRsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4zLjQtcjMifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTRiMTA2ZTZjM2ZkNzRjMWMiLCJjcGUiOiJjcGU6Mi4zOmE6bGlic3NsMS4xOmxpYnNzbDEuMToxLjEuMW4tcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiU1NMIHNoYXJlZCBsaWJyYXJpZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJzc2wxLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNTQwNjcyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJvcGVuc3NsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExL0taMDBxREhXWjVjajNBV0cvRFBkQUNSTllJPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjIxMzIwOSJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTIwZGMyMGNiYjZkYmVhNiIsImNwZSI6ImNwZToyLjM6YTptdXNsOm11c2w6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNjIyNTkyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExRGViMGpOeXRrcmpQVzROL2VLTFo0M0J3T2x3PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzgzMTUyIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbXVzbC11dGlsc0AxLjIuMi1yNz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW11c2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MzVjMzY4MDU3N2ZhZTBkZiIsImNwZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wtdXRpbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTQzMzYwIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExUDUwY2ZKaVNzSG9xc1lSVHlPRU9sSmlMbjNvPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic2NhbmVsZiBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjM2NzIzIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2wtdXRpbHNAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1mMmQ0MjYzNzIzNTY2MDJkIiwiY3BlIjoiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTY2FuIEVMRiBiaW5hcmllcyBmb3Igc3R1ZmYiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3dpa2kuZ2VudG9vLm9yZy93aWtpL0hhcmRlbmVkL1BhWF9VdGlsaXRpZXMifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNjYW5lbGYiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQyMDgifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InBheC11dGlscyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMTEvZFpEa1VJY0tUM2xuSENOcHN4dGJzSE5Kbz0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzY4MzAifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3NzbF9jbGllbnRAMS4zNC4xLXI1P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YnVzeWJveFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xZTE4MTJkZWI2NjY5MmM1IiwiY3BlIjoiY3BlOjIuMzphOnNzbC1jbGllbnQ6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiRVh0ZXJuYWwgc3NsX2NsaWVudCBmb3IgYnVzeWJveCB3Z2V0IiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNzbF9jbGllbnQiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbF9jbGllbnQ6MS4zNC4xLXI1Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6c3NsX2NsaWVudDpzc2wtY2xpZW50OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjg2NzIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImJ1c3lib3gifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFMUXBhVFlaVHpSL2tnS3ZSd0x1WThkRUZZUDQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGlidGxzLnNvLjIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjQ3MTYifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc3NsX2NsaWVudEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1iMzk5MDhlNzI5NzQ2MDkiLCJjcGUiOiJjcGU6Mi4zOmE6emxpYjp6bGliOjEuMi4xMi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBIGNvbXByZXNzaW9uL2RlY29tcHJlc3Npb24gTGlicmFyeSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vemxpYi5uZXQvIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IlpsaWIifX1dLCJuYW1lIjoiemxpYiIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3NDE0ODgwODY3OWY0N2FkOTZkYzk5ZTgzZWY3M2FjZmRlZWMxNjQyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIxMTA1OTIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InpsaWIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFIa3AyekgyYXlBV25RNzNrMFJkMjcwY1FCQjQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjUzNDg4In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4yLjEyLXIwIn0seyJkZXNjcmlwdGlvbiI6IkFscGluZSBMaW51eCB2My4xNSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6Imlzc3VlLXRyYWNrZXIiLCJ1cmwiOiJodHRwczovL2J1Z3MuYWxwaW5lbGludXgub3JnLyJ9LHsidHlwZSI6IndlYnNpdGUiLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZy8ifV0sIm5hbWUiOiJhbHBpbmUiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpkaXN0cm86aWQiLCJ2YWx1ZSI6ImFscGluZSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnByZXR0eU5hbWUiLCJ2YWx1ZSI6IkFscGluZSBMaW51eCB2My4xNSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnZlcnNpb25JRCIsInZhbHVlIjoiMy4xNS40In1dLCJzd2lkIjp7Im5hbWUiOiJhbHBpbmUiLCJ0YWdJZCI6ImFscGluZSIsInZlcnNpb24iOiIzLjE1LjQifSwidHlwZSI6Im9wZXJhdGluZy1zeXN0ZW0iLCJ2ZXJzaW9uIjoiMy4xNS40In1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6ImZjNjM4NjY1ZDQwNDdlYTEiLCJuYW1lIjoiYWxwaW5lOmxhdGVzdCIsInR5cGUiOiJjb250YWluZXIiLCJ2ZXJzaW9uIjoic2hhMjU2OmE3NzdjOWM2NmJhMTc3Y2NmZWEyM2YyYTIxNmZmNjcyMWU3OGE2NjJjZDE3MDE5NDg4YzQxNzEzNTI5OWNkODkifSwidGltZXN0YW1wIjoiMjAyMi0wNC0xMVQxNzoxMTo0MS0wNzowMCIsInRvb2xzIjpbeyJuYW1lIjoic3lmdCIsInZlbmRvciI6ImFuY2hvcmUiLCJ2ZXJzaW9uIjoiMC40My4yIn1dfSwic2VyaWFsTnVtYmVyIjoidXJuOnV1aWQ6ZWQ5MjQyNmYtYTkxNi00NDljLTgzYjgtZDcwODYwMTM3NzczIiwic3BlY1ZlcnNpb24iOiIxLjQiLCJ2ZXJzaW9uIjoxfX0=","signatures":[{"keyid":"","sig":"MEQCIDq1ltJFSy/cIzUSQmnqcP4ttqBY6vc92Nld45QY12GoAiBbjJQmixgSc6Hm5fClMXZnoyWbZJjKouQDzX5bvtWd0w=="}]} ================================================ FILE: grype/pkg/testdata/alpine.att.json ================================================ {"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3N5ZnQuZGV2L2JvbSIsInN1YmplY3QiOlt7Im5hbWUiOiIiLCJkaWdlc3QiOnsic2hhMjU2IjoiNmFmMWIxMWJiYjE3ZjRjMzExZTI2OWRiNjUzMGU0ZGEyNzM4MjYyYWY1ZmQ5MDY0Y2NkZjEwOWI3NjU4NjBmYiJ9fV0sInByZWRpY2F0ZSI6eyJhcnRpZmFjdFJlbGF0aW9uc2hpcHMiOlt7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWZhOTRlYThlODc0ZWU0OSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNTU5YTlkNTFlMzNkMjQxMCIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM2Zjk4Yzg1N2E2OTFjYyIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYzc5NWQzZjdlYmQ2NGU4NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGYyMmVkMWEzMWZkYmI2OCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTg1YWYxNGQ1MDMyNTc0NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGJlNzE2ZTA1ZjkxMWZmOSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWEzNTc2ZTRhYWRiODJmMSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNWEyNWQwZGJlODBhYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1NTU4Zjc2MDM4ZDFjOTdlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzODcxMmFkZWFlZGI3MzE2IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGRkOTg1ZGNlNGNkNWE1IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OTBjYzI2MzU4N2UyYzlkIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyNmEyMGYxMDZmYTMyNzgzIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmMjlhZjI0NzU1MGRjYWM4IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTZkNmUwZWE5NTc1MWI3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiOGYxZTRhNjJiMjE2NjNiIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0NTZhNzAyZjE2NzMzYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGUzZjQxYTc3YjQyODA3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNDFkYWQ4YTYxZmY4M2I2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjOWJlYzUzYzBhM2UyMTA2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNWRmZDI5MTQ4MWM2OWUwIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjMTVmOTJkZGM3N2MxYWU0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkMGMyNDUzZjk4YjMxMWQ4IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYzY2MzYyYjI1YzY0MzRhIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzVjYmI3OGNjNzQwZmJlIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkZWMyMzliNTY1ZDE5YjY0IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNjA1NDJmYTMzZGFlYjA1IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyZDI0YzQwYTg4YjRjMmVjIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1ZGIzOGRkY2U4OTEwNWU3IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwicGFyZW50IjoiYzJlM2NlN2I5ZTcyZDBhZSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4NTI0ZGNmMDI5MzZkODE3IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYzYyNGZiZjM5Y2E3ZDY0IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYWY0MTJiMTBmZjJjNWZiIiwicGFyZW50IjoiMWYyOGRlMTIwMDczZDc4YSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyMzcwMjZkMTM0Yjg0ZDZmIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6IjNjYTdjYmZkNTkzMGQ5MGIiLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWFjYzQ2ZjlmOWQyNDA1OCIsInBhcmVudCI6IjUzYTkwOWY0ZDg3MmI5MCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNjJmMzI0MDM2NTYxNjQzIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6ImEyOWI0NWI1YzAyZDAwMDciLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifV0sImFydGlmYWN0cyI6W3siY3BlcyI6WyJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI3ZjAyNDFhNzBiNjgxODE5IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImZpbGVzIjpbeyJwYXRoIjoiL2RldiJ9LHsicGF0aCI6Ii9kZXYvcHRzIn0seyJwYXRoIjoiL2Rldi9zaG0ifSx7InBhdGgiOiIvZXRjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTExUTdoTmU4UXBEUzUzMWd1cUNkclhCem9BL289In0sInBhdGgiOiIvZXRjL2ZzdGFiIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEzSytvbEpnNWF5ekhTVk5Va2dnWkpYdUIrOVk9In0sInBhdGgiOiIvZXRjL2dyb3VwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE2blZ3WVZYUC90Q2h2VVBkdWtWRDJpZlhPbWM9In0sInBhdGgiOiIvZXRjL2hvc3RuYW1lIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFCRDZ6SktaVFJXeXFHblBpNHRTZmQza3JzTVU9In0sInBhdGgiOiIvZXRjL2hvc3RzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFUc3RoYmhXN1F6V1JlMUUvTkt3VE91RDRwSGM9In0sInBhdGgiOiIvZXRjL2luaXR0YWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXRvb2dqVWlwSEdjTWdFQ2dQSlg2NFN3VVQxTT0ifSwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExWG1kdVZWTlVSSFEyN1R2WXAxTHI1VE10RmNBPSJ9LCJwYXRoIjoiL2V0Yy9tb3RkIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFraWxqaFhYSDFMbFFyb0hzRUpJa1BaZzJlaXc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL210YWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVGNodXVMVWZ1cjBpenZmWlFaeGdOL0xKaEI4PSJ9LCJwYXRoIjoiL2V0Yy9wYXNzd2QifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVZtSFBXUGpqdno0b0NzYm1ZQ1VCNHVXcFNrYz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExb21LbHAzdmdHcTJacVl6eUQvS0hOZG84ckRjPSJ9LCJwYXRoIjoiL2V0Yy9wcm90b2NvbHMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTlXTEN2NUl0S2c0TUg3UldmTlJoMUk3YnlRYz0ifSwicGF0aCI6Ii9ldGMvc2VydmljZXMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWx0clBJQVcyekhlRGlhanNleDJCZG1xM3VxQT0ifSwib3duZXJHaWQiOiI0MiIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL3NoYWRvdyIsInBlcm1pc3Npb25zIjoiNjQwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFvam0yWWRwQ0o2Qi9hcEdEYVovU2RiMnhKa0E9In0sInBhdGgiOiIvZXRjL3NoZWxscyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExNHVwejN0Zm5OeFprSUVzVWhXbjdYb2l3OTZnPSJ9LCJwYXRoIjoiL2V0Yy9zeXNjdGwuY29uZiJ9LHsicGF0aCI6Ii9ldGMvYXBrIn0seyJwYXRoIjoiL2V0Yy9jb25mLmQifSx7InBhdGgiOiIvZXRjL2Nyb250YWJzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF2ZmsxYXBVV0k0eUxKR2hoTlJkMGtKaXhmdlk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QiLCJwZXJtaXNzaW9ucyI6IjYwMCJ9LHsicGF0aCI6Ii9ldGMvaW5pdC5kIn0seyJwYXRoIjoiL2V0Yy9tb2Rwcm9iZS5kIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFXVWJoNlRCWU5WSzdlNFkrdVV2THMvN3ZpcWs9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE0VGRnRkhrVGR0M3VRQytOQnRybnRPbm05bjQ9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYmxhY2tsaXN0LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXBuYXkvbmpuNm9sOWNDc3NMN0tpWlo4ZXRsYz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9pMzg2LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXluYkxuM0dZRHB2YWpiYS9sZHAxbmlheWVvZz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9LHsicGF0aCI6Ii9ldGMvbW9kdWxlcy1sb2FkLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQifSx7InBhdGgiOiIvZXRjL29wdCJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMifSx7InBhdGgiOiIvZXRjL3BlcmlvZGljLzE1bWluIn0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9kYWlseSJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMvaG91cmx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9tb250aGx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy93ZWVrbHkifSx7InBhdGgiOiIvZXRjL3Byb2ZpbGUuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMzVPV3NDenp2bkIyZm1GeDYya2JxbTFBeDFrPSJ9LCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvUkVBRE1FIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEwd0wyM0d1U0NWZnVtTVJnYWthYlVJNkVzU2s9In0sInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9jb2xvcl9wcm9tcHQuc2guZGlzYWJsZWQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVM4aitXVzcxbVd4ZlZ5OHl0aHFVN0hVVm9Cdz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9LHsicGF0aCI6Ii9ldGMvc3lzY3RsLmQifSx7InBhdGgiOiIvaG9tZSJ9LHsicGF0aCI6Ii9saWIifSx7InBhdGgiOiIvbGliL2Zpcm13YXJlIn0seyJwYXRoIjoiL2xpYi9tZGV2In0seyJwYXRoIjoiL2xpYi9tb2R1bGVzLWxvYWQuZCJ9LHsicGF0aCI6Ii9saWIvc3lzY3RsLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUhwRWx6VzF4RWdtS2ZFUnRUeTdvb21tbnE2Yz0ifSwicGF0aCI6Ii9saWIvc3lzY3RsLmQvMDAtYWxwaW5lLmNvbmYifSx7InBhdGgiOiIvbWVkaWEifSx7InBhdGgiOiIvbWVkaWEvY2Ryb20ifSx7InBhdGgiOiIvbWVkaWEvZmxvcHB5In0seyJwYXRoIjoiL21lZGlhL3VzYiJ9LHsicGF0aCI6Ii9tbnQifSx7InBhdGgiOiIvb3B0In0seyJwYXRoIjoiL3Byb2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvcm9vdCIsInBlcm1pc3Npb25zIjoiNzAwIn0seyJwYXRoIjoiL3J1biJ9LHsicGF0aCI6Ii9zYmluIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFxamtkeVJKY1libEdDNlJNcVVSNEJkYjVnMTA9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvc2Jpbi9ta21udGRpcnMiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii9zcnYifSx7InBhdGgiOiIvc3lzIn0seyJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsicGF0aCI6Ii91c3IvbGliL21vZHVsZXMtbG9hZC5kIn0seyJwYXRoIjoiL3Vzci9sb2NhbCJ9LHsicGF0aCI6Ii91c3IvbG9jYWwvYmluIn0seyJwYXRoIjoiL3Vzci9sb2NhbC9saWIifSx7InBhdGgiOiIvdXNyL2xvY2FsL3NoYXJlIn0seyJwYXRoIjoiL3Vzci9zYmluIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvbWFuIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9taXNjIn0seyJwYXRoIjoiL3ZhciJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMS9TTlp6LzhjSzJkU0tLK2NKcFZyWkl1RjRRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9ydW4iLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL2VtcHR5IiwicGVybWlzc2lvbnMiOiI1NTUifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL21pc2MifSx7InBhdGgiOiIvdmFyL2xvY2FsIn0seyJwYXRoIjoiL3Zhci9sb2NrIn0seyJwYXRoIjoiL3Zhci9sb2NrL3N1YnN5cyJ9LHsicGF0aCI6Ii92YXIvbG9nIn0seyJwYXRoIjoiL3Zhci9tYWlsIn0seyJwYXRoIjoiL3Zhci9vcHQifSx7InBhdGgiOiIvdmFyL3Nwb29sIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFkemJkYXpZWkEyblR6U0lHM1l5Tnc3ZDRKdWM9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3Nwb29sL21haWwiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvc3Bvb2wvY3JvbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT0ZadCtaTXA3ajBHbnkwcnFTS3VXSnlxWW1BPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9zcG9vbC9jcm9uL2Nyb250YWJzIiwicGVybWlzc2lvbnMiOiI3NzcifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSIsImluc3RhbGxlZFNpemUiOjQxMzY5NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJhbHBpbmUtYmFzZWxheW91dCIsInBhY2thZ2UiOiJhbHBpbmUtYmFzZWxheW91dCIsInB1bGxDaGVja3N1bSI6IlExRXltUzZyQWdtR3M3WFlocWR5RW9pV2dFWjZBPSIsInB1bGxEZXBlbmRlbmNpZXMiOiIvYmluL3NoIHNvOmxpYmMubXVzbC14ODZfNjQuc28uMSIsInNpemUiOjIxMTAxLCJ1cmwiOiJodHRwczovL2dpdC5hbHBpbmVsaW51eC5vcmcvY2dpdC9hcG9ydHMvdHJlZS9tYWluL2FscGluZS1iYXNlbGF5b3V0IiwidmVyc2lvbiI6IjMuMi4wLXIxOCJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJhbHBpbmUtYmFzZWxheW91dCIsInB1cmwiOiJwa2c6YWxwaW5lL2FscGluZS1iYXNlbGF5b3V0QDMuMi4wLXIxOD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjMuMi4wLXIxOCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmVfa2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiIxOTk3ZGQxYzhmZjcyZGMyIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJNSVQiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlB1YmxpYyBrZXlzIGZvciBBbHBpbmUgTGludXggcGFja2FnZXMiLCJmaWxlcyI6W3sicGF0aCI6Ii9ldGMifSx7InBhdGgiOiIvZXRjL2FwayJ9LHsicGF0aCI6Ii9ldGMvYXBrL2tleXMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU92Q0ZTTzk0ejk3YzgwbUlEQ3hxR2toMk9nND0ifSwicGF0aCI6Ii9ldGMvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy00YTZhMDg0MC5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF2N1lXWll6QVdvY2xhTERJNDVqRWd1STdZTjA9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTm5HdURzZFFPeDRaTllmQjNOOTdlTHlHUGtJPSJ9LCJwYXRoIjoiL2V0Yy9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNjFjZWNiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWxabFRFU05yZWxXVE5rTC9vUXptQVU4YTk5QT0ifSwicGF0aCI6Ii9ldGMvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTY1ZWU1OS5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFXTlc2U3k4N0hwSjNJZGVtUXk4cGp1MzNLbXM9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL3NoYXJlIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFPdkNGU085NHo5N2M4MG1JREN4cUdraDJPZzQ9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExdjdZV1pZekFXb2NsYUxESTQ1akVndUk3WU4wPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNDNlZjRiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUJUcVMrSC9VVXloUXV6SHdpQmw0NytCVEt1VT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjRkMjdiYi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFObkd1RHNkUU94NFpOWWZCM045N2VMeUdQa0k9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT2F4ZGNzYTZBWW9QZExpMFU0bE8zSjJ3ZTE4PSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4MTk5ZGNjLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXlQcStzdTY1a3NOb3gzdVhCK0RSN1AxOCtRVT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01OGNiYjQ3Ni5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFNcFpETlgwTGVMSHZTT3dWVXlYaVh4MTFOTjA9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExZ2xDUS9lSmJ2QTV4cWNzd2RqRnJXdjVGbmswPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTVlNjljYTUwLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVhVZERFb05UdGpsdnJTK2l1bms2emlGZ0lwVT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MGFjMjA5OS5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsWmxURVNOcmVsV1ROa0wvb1F6bUFVOGE5OUE9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExV05XNlN5ODdIcEozSWRlbVF5OHBqdTMzS21zPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjY2ZTNmLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUk5RHk2aHJ5YWNMMllXWGcrS2xFNld2d0VkND0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhOTcyNC5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFOU25zZ21jTWJVNGc3ajVKYU5zMHRWSHBIVkE9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVmFNQkJrNFJ4djZib1BMS0YrSTA4NVE4eTJFPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFjM2JjLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTNoSkJNSEFVcXVQYnA1anBBUEZqUUkyWTF2UT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhZGZlYi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFWL2E1UDlwS1JKYjZ0aWhFM2U4TzZ4YVBnTFU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExM3dMSnJjS1FhanFsNWExcDlRNDVVK1pYRU5BPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmRiMzBkLnJzYS5wdWIifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FhcmNoNjQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTdqOW5XSmtRK3dmSXVWUXpJRnJtRlo3ZlNPYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWFyY2g2NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4MTk5ZGNjLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExc25yK1ExVWJmSHlDci9jbW10VnZNSVM3U0dzPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hYXJjaDY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hcm1oZiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVTlRdHNkTityWVo5Wmg3NkVmWHkwMEpaSE1nPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hcm1oZi9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNGQyN2JiLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExYkMrQWRRMHFXQlRtZWZYaUkwUHZtWU9Kb1ZRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hcm1oZi9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmE5NzI0LnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtdjcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVU5UXRzZE4rcllaOVpoNzZFZlh5MDBKWkhNZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtdjcvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjRkMjdiYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXhiSVZ1N1Njd3FHSHhYR3dJMjJhU2U1T2RVWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtdjcvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhZGZlYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL21pcHM2NCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExaENaZEZ4K0x2emJMdFBzNzUzamU3OGdFRUJRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9taXBzNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ZTY5Y2E1MC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3BwYzY0bGUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXQyMWRoQ0xiVEptQUhYU0NlT01xLzJ2ZlNnbz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcHBjNjRsZS9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4Y2JiNDc2LnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExUFM5ek5JUEphbkM4cWNzYzVxYXJFV3FoVjVRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9wcGM2NGxlL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9yaXNjdjY0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFOVlBiWmF2YVhwc0l0RndRWURXYnBvcjd5WUU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3Jpc2N2NjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MGFjMjA5OS5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVU2dGZ1S1J5NUo4QzZpYUtQTVphVC9lOHRiQT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcmlzY3Y2NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmRiMzBkLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvczM5MHgifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNqYlYycjJ3MEloMnZ3ZHpDNEpxNlVJN2NNUT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvczM5MHgvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01OGU0ZjE3ZC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWwwOXhhN1JuYk9JQzFkSTlGcWJhQ2ZTL0dYWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvczM5MHgvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhYzNiYy5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4NiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExSWk1MWk3TnJjNHVmdDE0SGhxdWdhVXFkSDY0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy00YTZhMDg0MC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVk0OWVWeGhwdmZ0YlEzeUFkdmxMZmNyUExUVT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFIamR2Y1ZrcEJaenIxYVNlM3A3b1FmQXRtL0U9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Ni9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjY2ZTNmLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2XzY0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFJaTUxaTdOcmM0dWZ0MTRIaHF1Z2FVcWRINjQ9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Nl82NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTRhNmEwODQwLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExQVVGWStmd1NCVGNyWWV0alQ3Tkh2YWZyU1FjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODZfNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjYxY2VjYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXFLQTIzVnpNVURsZStEcW5ycjVLeitYdnR5ND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2XzY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiJhYWI2OGY4YzlhYjQzNGE0NjcxMGRlOGUxMmZiMzIwNmUyOTMwYTU5IiwiaW5zdGFsbGVkU2l6ZSI6MTU5NzQ0LCJsaWNlbnNlIjoiTUlUIiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwib3JpZ2luUGFja2FnZSI6ImFscGluZS1rZXlzIiwicGFja2FnZSI6ImFscGluZS1rZXlzIiwicHVsbENoZWNrc3VtIjoiUTFrREYyc3RLbzNlL1J1bWxBOFpyUmZDd2RTdjg9IiwicHVsbERlcGVuZGVuY2llcyI6IiIsInNpemUiOjEzMzYyLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZyIsInZlcnNpb24iOiIyLjQtcjEifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYWxwaW5lLWtleXMiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUta2V5c1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyLjQtcjEifSx7ImNwZXMiOlsiY3BlOjIuMzphOmFway10b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YXBrLXRvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGtfdG9vbHM6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFwa190b29sczphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YXBrOmFway10b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGs6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjVlZjY2YTMzNWRkYzAzYTYiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiQWxwaW5lIFBhY2thZ2UgS2VlcGVyIC0gcGFja2FnZSBtYW5hZ2VyIGZvciBhbHBpbmUiLCJmaWxlcyI6W3sicGF0aCI6Ii9ldGMifSx7InBhdGgiOiIvZXRjL2FwayJ9LHsicGF0aCI6Ii9ldGMvYXBrL2tleXMifSx7InBhdGgiOiIvZXRjL2Fway9wcm90ZWN0ZWRfcGF0aHMuZCJ9LHsicGF0aCI6Ii9saWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVM1REw2REZPbWpqTnhBR05zc2ZqNG5VaThYVT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliYXBrLnNvLjMuMTIuMCIsInBlcm1pc3Npb25zIjoiNzU1In0seyJwYXRoIjoiL3NiaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU8xcEJVQjJrVE0vTHFMOGQwVDk4Q0ViWllxdz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9zYmluL2FwayIsInBlcm1pc3Npb25zIjoiNzU1In0seyJwYXRoIjoiL3ZhciJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL2FwayJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiMWFjM2MxYmIyOWVlZmYwODNjNjIxY2Y2YjI3YWQxMmFiOTNjYjczYSIsImluc3RhbGxlZFNpemUiOjMxMTI5NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJhcGstdG9vbHMiLCJwYWNrYWdlIjoiYXBrLXRvb2xzIiwicHVsbENoZWNrc3VtIjoiUTEzZlBkK0ZSWGFMd3lOa2xWbitxdUZXRHlrbk09IiwicHVsbERlcGVuZGVuY2llcyI6Im11c2xcdTAwM2U9MS4yIGNhLWNlcnRpZmljYXRlcy1idW5kbGUgc286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEgc286bGlic3NsLnNvLjEuMSBzbzpsaWJ6LnNvLjEiLCJzaXplIjoxMjAzNzcsInVybCI6Imh0dHBzOi8vZ2l0bGFiLmFscGluZWxpbnV4Lm9yZy9hbHBpbmUvYXBrLXRvb2xzIiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJhcGstdG9vbHMiLCJwdXJsIjoicGtnOmFscGluZS9hcGstdG9vbHNAMi4xMi43LXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YXBrLXRvb2xzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6YnVzeWJveDpidXN5Ym94OjEuMzQuMS1yNDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6ImYyMTMyZThkNmNmZTAwNmEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiU2l6ZSBvcHRpbWl6ZWQgdG9vbGJveCBvZiBtYW55IGNvbW1vbiBVTklYIHV0aWxpdGllcyIsImZpbGVzIjpbeyJwYXRoIjoiL2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExOEc5WGVOR0FVQTQzdmlVS3NsbWRpbjJ6RDI4PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2Jpbi9idXN5Ym94IiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXBjZlRmRE5FYk5LUWMyczF0aWE3ZGEwNU04UT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9iaW4vc2giLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii9ldGMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMW1COTVIcTJOVVRaNTk5UkRpU3NqOXc1RnJPVT0ifSwicGF0aCI6Ii9ldGMvc2VjdXJldHR5In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFFZ0xGamo2N291M2VNcXA0bTNyMlpqblE3UVU9In0sInBhdGgiOiIvZXRjL3VkaGNwZC5jb25mIn0seyJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVHlseUNJTlZtblMrQS9UZWFkNHZaaEU3QmtzPSJ9LCJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZC9hY3BpZCJ9LHsicGF0aCI6Ii9ldGMvbmV0d29yayJ9LHsicGF0aCI6Ii9ldGMvbmV0d29yay9pZi1kb3duLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtcG9zdC1kb3duLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtcG9zdC11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS1kb3duLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtcHJlLXVwLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtdXAuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT1JmK2xQUkt1WWdka0JCY0tvZXZSMXQ2MFE0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQvZGFkIiwicGVybWlzc2lvbnMiOiI3NzUifSx7InBhdGgiOiIvc2JpbiJ9LHsib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii90bXAiLCJwZXJtaXNzaW9ucyI6IjE3NzcifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9zYmluIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvdWRoY3BjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF0OXZpci9aclgzbmJTSVlUOUJETFdaZW5rVlE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYy9kZWZhdWx0LnNjcmlwdCIsInBlcm1pc3Npb25zIjoiNzU1In0seyJwYXRoIjoiL3ZhciJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL3VkaGNwZCJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiYTE2MDU5OGQ2MmEwYWM1NTg1MWFhZDE5MjUxYzAxYTFiYjVmYjIyYyIsImluc3RhbGxlZFNpemUiOjk0NjE3NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6ImJ1c3lib3giLCJwdWxsQ2hlY2tzdW0iOiJRMUg2YXBoZGhZWjl1c1J2bVZqOVV0NVhRb2g5OD0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6NTAwNjA2LCJ1cmwiOiJodHRwczovL2J1c3lib3gubmV0LyIsInZlcnNpb24iOiIxLjM0LjEtcjQifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYnVzeWJveCIsInB1cmwiOiJwa2c6YWxwaW5lL2J1c3lib3hAMS4zNC4xLXI0P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YnVzeWJveFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjM0LjEtcjQifSx7ImNwZXMiOlsiY3BlOjIuMzphOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlc19idW5kbGU6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlc19idW5kbGU6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhLWNlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpjYV9jZXJ0aWZpY2F0ZXM6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2E6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI4Y2ViMjdhMTJjMGJmZTdiIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJNUEwtMi4wIiwiQU5EIiwiTUlUIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJQcmUgZ2VuZXJhdGVkIGJ1bmRsZSBvZiBNb3ppbGxhIGNlcnRpZmljYXRlcyIsImZpbGVzIjpbeyJwYXRoIjoiL2V0YyJ9LHsicGF0aCI6Ii9ldGMvc3NsIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFOajZnVEJka1pwVEZXL29iSkdkcGZ2SzBTdEE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL3NzbC9jZXJ0LnBlbSIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL2V0Yy9zc2wvY2VydHMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMW0yY0pvZm9OWnRDQ0s0MXl2alRnWDRLN2R2cz0ifSwicGF0aCI6Ii9ldGMvc3NsL2NlcnRzL2NhLWNlcnRpZmljYXRlcy5jcnQifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6IjcwOWI3MGJjYjcyNzM4Y2ZlZGM1MTBiYmEwODE0MWIwMTIwMzgxNjciLCJpbnN0YWxsZWRTaXplIjoyMjExODQsImxpY2Vuc2UiOiJNUEwtMi4wIEFORCBNSVQiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJvcmlnaW5QYWNrYWdlIjoiY2EtY2VydGlmaWNhdGVzIiwicGFja2FnZSI6ImNhLWNlcnRpZmljYXRlcy1idW5kbGUiLCJwdWxsQ2hlY2tzdW0iOiJRMVNWQVd1V0hkUEh2YkJoTFRrQVo2MC8xV3NtST0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiIiwic2l6ZSI6MTE5NzQ4LCJ1cmwiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy9hYm91dC9nb3Zlcm5hbmNlL3BvbGljaWVzL3NlY3VyaXR5LWdyb3VwL2NlcnRzLyIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHVybCI6InBrZzphbHBpbmUvY2EtY2VydGlmaWNhdGVzLWJ1bmRsZUAyMDIxMTIyMC1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWNhLWNlcnRpZmljYXRlc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiMTIzN2UwYzMxNWYyNjkwMiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiQlNELTItQ2xhdXNlIiwiQU5EIiwiQlNELTMtQ2xhdXNlIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJNZXRhIHBhY2thZ2UgdG8gcHVsbCBpbiBjb3JyZWN0IGxpYmMiLCJmaWxlcyI6W10sImdpdENvbW1pdE9mQXBrUG9ydCI6IjYwNDI0MTMzYmUyZTc5YmJmZWZmM2Q1ODE0N2EyMjg4NmY4MTdjZTIiLCJpbnN0YWxsZWRTaXplIjo0MDk2LCJsaWNlbnNlIjoiQlNELTItQ2xhdXNlIEFORCBCU0QtMy1DbGF1c2UiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJvcmlnaW5QYWNrYWdlIjoibGliYy1kZXYiLCJwYWNrYWdlIjoibGliYy11dGlscyIsInB1bGxDaGVja3N1bSI6IlExZVkzajY3Vi9QaWowQ0FnSFJwTmZJVG9KbHlJPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJtdXNsLXV0aWxzIiwic2l6ZSI6MTQ4NSwidXJsIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmciLCJ2ZXJzaW9uIjoiMC43LjItcjMifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibGliYy11dGlscyIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmMtdXRpbHNAMC43LjItcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1saWJjLWRldlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGliY3J5cHRvMS4xOmxpYmNyeXB0bzEuMToxLjEuMW4tcjA6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI3YTJjZjcyN2NiYWI4MDc0IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJPcGVuU1NMIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJDcnlwdG8gbGlicmFyeSBmcm9tIG9wZW5zc2wiLCJmaWxlcyI6W3sicGF0aCI6Ii9ldGMifSx7InBhdGgiOiIvZXRjL3NzbDEuMSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT2VVeU9EWVdlMmhCd0JtMHF3czJvRFcvV1FjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY2VydC5wZW0iLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTm9CRjdSTUlpVDlmQ1hMai9tYkRoK3BuTDlvPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY2VydHMiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExYTdWUFI4NXdydVhGbU5GWkUvREJhMFB5enEwPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY3RfbG9nX2xpc3QuY25mIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMW9saDhUcGRBaTJRblRsNEZLM1RqZFVpU3dUbz0ifSwicGF0aCI6Ii9ldGMvc3NsMS4xL2N0X2xvZ19saXN0LmNuZi5kaXN0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF3R3V4VkVPSzlpR0xqMWk4RDNCU0JuVDdNSkE9In0sInBhdGgiOiIvZXRjL3NzbDEuMS9vcGVuc3NsLmNuZiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExd0d1eFZFT0s5aUdMajFpOEQzQlNCblQ3TUpBPSJ9LCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYuZGlzdCJ9LHsicGF0aCI6Ii9ldGMvc3NsMS4xL3ByaXZhdGUifSx7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEwMGhyT1lyY1d6Q3YwS0c4SG5Sd3BIT1RGc0U9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xpYmNyeXB0by5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVDJzaStjN3RzN3NnRHhRWXZlNEIzaTFEZ28wPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGliY3J5cHRvLnNvLjEuMSIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUxwbzIvUkxUZjVWbEhtVVFYeXY1YzFQT3BHWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xL2FmYWxnLnNvIiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWxsUHFIT3UzeWVBVEpoVm1heDNhSlY5dzQ4dz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xL2NhcGkuc28iLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExL0doakVRMTdLU1ZiZ3lwRDA2VTVVMnN3a3NZPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvcGFkbG9jay5zbyIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiI0NTVlOTY2ODk5YTkzNThmYzk0ZjViY2U2MzNhZmU4YTE5NDIwOTVjIiwiaW5zdGFsbGVkU2l6ZSI6Mjc0MDIyNCwibGljZW5zZSI6Ik9wZW5TU0wiLCJtYWludGFpbmVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJvcGVuc3NsIiwicGFja2FnZSI6ImxpYmNyeXB0bzEuMSIsInB1bGxDaGVja3N1bSI6IlExckFzTGNiWTk2VCtUcW91ME1IMHlQUTExaEdRPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjoxMjA4MjI4LCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8iLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYmNyeXB0bzEuMSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMS4xbi1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGlicmV0bHM6bGlicmV0bHM6My4zLjQtcjM6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI5MWRiMTVkODA0ZmVkZTU5IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJJU0MiLCJBTkQiLCIoQlNELTMtQ2xhdXNlIiwiT1IiLCJNSVQpIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJwb3J0IG9mIGxpYnRscyBmcm9tIGxpYnJlc3NsIHRvIG9wZW5zc2wiLCJmaWxlcyI6W3sicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExbk5FQzlUL3Q2VytFY20wRHhxTVVuUnZjVDZrPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlidGxzLnNvLjIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExZVNXbm11c1NjbDZ3R2NrdGt3Mi84Y3I5ekZFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlidGxzLnNvLjIuMC4zIiwicGVybWlzc2lvbnMiOiI3NTUifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6IjkxYzdhOWYzYWEyOTZiNmQ0NjJjNTYzNGU3NjU4ZWJkYmZmNjViYjkiLCJpbnN0YWxsZWRTaXplIjo4NjAxNiwibGljZW5zZSI6IklTQyBBTkQgKEJTRC0zLUNsYXVzZSBPUiBNSVQpIiwibWFpbnRhaW5lciI6IkFyaWFkbmUgQ29uaWxsIFx1MDAzY2FyaWFkbmVAZGVyZWZlcmVuY2VkLm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJsaWJyZXRscyIsInBhY2thZ2UiOiJsaWJyZXRscyIsInB1bGxDaGVja3N1bSI6IlExWjkvdjVVVnNSUmtyWU5kcTNwakZBYkN1Z1U4PSIsInB1bGxEZXBlbmRlbmNpZXMiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIHNvOmxpYmMubXVzbC14ODZfNjQuc28uMSBzbzpsaWJjcnlwdG8uc28uMS4xIHNvOmxpYnNzbC5zby4xLjEiLCJzaXplIjoyOTE4NSwidXJsIjoiaHR0cHM6Ly9naXQuY2F1c2FsLmFnZW5jeS9saWJyZXRscy8iLCJ2ZXJzaW9uIjoiMy4zLjQtcjMifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibGlicmV0bHMiLCJwdXJsIjoicGtnOmFscGluZS9saWJyZXRsc0AzLjMuNC1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYnJldGxzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjMuMy40LXIzIn0seyJjcGVzIjpbImNwZToyLjM6YTpsaWJzc2wxLjE6bGlic3NsMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjMwOTRjNGE2MTBiMGIwMGQiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk9wZW5TU0wiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlNTTCBzaGFyZWQgbGlicmFyaWVzIiwiZmlsZXMiOlt7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF4TmpqN2p4dk9qM2xEUmQzc1JYekhvd1RVc1E9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xpYnNzbC5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExOGozNXBlM3lwNkhPZ01paDF3bEdQMS9tbTJjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlic3NsLnNvLjEuMSIsInBlcm1pc3Npb25zIjoiNzc3In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiI0NTVlOTY2ODk5YTkzNThmYzk0ZjViY2U2MzNhZmU4YTE5NDIwOTVjIiwiaW5zdGFsbGVkU2l6ZSI6NTQwNjcyLCJsaWNlbnNlIjoiT3BlblNTTCIsIm1haW50YWluZXIiOiJUaW1vIFRlcmFzIFx1MDAzY3RpbW8udGVyYXNAaWtpLmZpXHUwMDNlIiwib3JpZ2luUGFja2FnZSI6Im9wZW5zc2wiLCJwYWNrYWdlIjoibGlic3NsMS4xIiwicHVsbENoZWNrc3VtIjoiUTEvS1owMHFESFdaNWNqM0FXRy9EUGRBQ1JOWUk9IiwicHVsbERlcGVuZGVuY2llcyI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSBzbzpsaWJjcnlwdG8uc28uMS4xIiwic2l6ZSI6MjEzMjA5LCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8iLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYnNzbDEuMSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMS4xbi1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bXVzbDptdXNsOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiNGFjNzEzNmI4NTM2Y2RlYSIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiTUlUIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJ0aGUgbXVzbCBjIGxpYnJhcnkgKGxpYmMpIGltcGxlbWVudGF0aW9uIiwiZmlsZXMiOlt7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEyYWR3cVFPam85ZEZsK1ZKRDJFY2Q5MDF2aEU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xkLW11c2wteDg2XzY0LnNvLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExN3lKM0pGTnlwQTRteGhKSnIwb3U2Q3pzSlZJPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJjLm11c2wteDg2XzY0LnNvLjEiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCIsImluc3RhbGxlZFNpemUiOjYyMjU5MiwibGljZW5zZSI6Ik1JVCIsIm1haW50YWluZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJtdXNsIiwicGFja2FnZSI6Im11c2wiLCJwdWxsQ2hlY2tzdW0iOiJRMURlYjBqTnl0a3JqUFc0Ti9lS0xaNDNCd09sdz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiIiwic2l6ZSI6MzgzMTUyLCJ1cmwiOiJodHRwczovL211c2wubGliYy5vcmcvIiwidmVyc2lvbiI6IjEuMi4yLXI3In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6Im11c2wiLCJwdXJsIjoicGtnOmFscGluZS9tdXNsQDEuMi4yLXI3P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09bXVzbFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bXVzbC11dGlsczptdXNsLXV0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbC11dGlsczptdXNsX3V0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbF91dGlsczptdXNsLXV0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbF91dGlsczptdXNsX3V0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbDptdXNsLXV0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbDptdXNsX3V0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiNTNhOTA5ZjRkODcyYjkwIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJNSVQiLCJCU0QiLCJHUEwyKyJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImZpbGVzIjpbeyJwYXRoIjoiL3NiaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUtqYTIrUE9aS3hFa1VPWnF3U2pDNmttYUVEND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9zYmluL2xkY29uZmlnIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU1pMjFCVGNMdE45Y1lQVjA3UDBhd0h5VDZYVT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL2dldGNvbmYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExblptREtLRlEydm9vSXRORExCbGVUOHg3T01BPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0ZW50IiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVE4VE9PZDVUbTJQdGtPNUVvb3d2aHZHQ0lKND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL2ljb252IiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXlGQWhHZ2dtTDdFUmdiSUE3S1F4eVR6ZjNrcz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL2xkZCIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiJiZjViYmZkYmY3ODAwOTJmMzg3YjdhYmU0MDFmYmZjZWRhOTBjODRkIiwiaW5zdGFsbGVkU2l6ZSI6MTQzMzYwLCJsaWNlbnNlIjoiTUlUIEJTRCBHUEwyKyIsIm1haW50YWluZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJtdXNsIiwicGFja2FnZSI6Im11c2wtdXRpbHMiLCJwdWxsQ2hlY2tzdW0iOiJRMVA1MGNmSmlTc0hvcXNZUlR5T0VPbEppTG4zbz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic2NhbmVsZiBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjozNjcyMywidXJsIjoiaHR0cHM6Ly9tdXNsLmxpYmMub3JnLyIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJtdXNsLXV0aWxzIiwicHVybCI6InBrZzphbHBpbmUvbXVzbC11dGlsc0AxLjIuMi1yNz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW11c2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4yLjItcjcifSx7ImNwZXMiOlsiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjFmMjhkZTEyMDA3M2Q3OGEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiU2NhbiBFTEYgYmluYXJpZXMgZm9yIHN0dWZmIiwiZmlsZXMiOlt7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNGZTU0UmJkZlQ0Q05pbVltNDFEMUR2K05zZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL3NjYW5lbGYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyIsImluc3RhbGxlZFNpemUiOjk0MjA4LCJsaWNlbnNlIjoiR1BMLTIuMC1vbmx5IiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwib3JpZ2luUGFja2FnZSI6InBheC11dGlscyIsInBhY2thZ2UiOiJzY2FuZWxmIiwicHVsbENoZWNrc3VtIjoiUTExL2RaRGtVSWNLVDNsbkhDTnBzeHRic0hOSm89IiwicHVsbERlcGVuZGVuY2llcyI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSIsInNpemUiOjM2ODMwLCJ1cmwiOiJodHRwczovL3dpa2kuZ2VudG9vLm9yZy93aWtpL0hhcmRlbmVkL1BhWF9VdGlsaXRpZXMiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoic2NhbmVsZiIsInB1cmwiOiJwa2c6YWxwaW5lL3NjYW5lbGZAMS4zLjMtcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1wYXgtdXRpbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSx7ImNwZXMiOlsiY3BlOjIuMzphOnNzbC1jbGllbnQ6c3NsLWNsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbF9jbGllbnQ6MS4zNC4xLXI0Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsX2NsaWVudDpzc2wtY2xpZW50OjEuMzQuMS1yNDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsX2NsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2w6c3NsLWNsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2w6c3NsX2NsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiJjMmUzY2U3YjllNzJkMGFlIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IkVYdGVybmFsIHNzbF9jbGllbnQgZm9yIGJ1c3lib3ggd2dldCIsImZpbGVzIjpbeyJwYXRoIjoiL3VzciJ9LHsicGF0aCI6Ii91c3IvYmluIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE1dWVGL1AvRW1BVlZtVUoxUFhCQWtwQ0FYN2M9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2Jpbi9zc2xfY2xpZW50IiwicGVybWlzc2lvbnMiOiI3NTUifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImExNjA1OThkNjJhMGFjNTU4NTFhYWQxOTI1MWMwMWExYmI1ZmIyMmMiLCJpbnN0YWxsZWRTaXplIjoyODY3MiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6InNzbF9jbGllbnQiLCJwdWxsQ2hlY2tzdW0iOiJRMTNHdzVIRWVMWmorUWhqN0hSbXd6aTBVT0Fndz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYnRscy5zby4yIiwic2l6ZSI6NDcwOSwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8iLCJ2ZXJzaW9uIjoiMS4zNC4xLXI0In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6InNzbF9jbGllbnQiLCJwdXJsIjoicGtnOmFscGluZS9zc2xfY2xpZW50QDEuMzQuMS1yND9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWJ1c3lib3hcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4zNC4xLXI0In0seyJjcGVzIjpbImNwZToyLjM6YTp6bGliOnpsaWI6MS4yLjExLXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiYmUwNmRmNmUzYmJiZjBhYiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiWmxpYiJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiQSBjb21wcmVzc2lvbi9kZWNvbXByZXNzaW9uIExpYnJhcnkiLCJmaWxlcyI6W3sicGF0aCI6Ii9saWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWEySDhoUDI0cnlDQUdmOGZuYzFOaGE5SUlIYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliei5zby4xIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU00T0hMVVlodkhvbHFnTUIwVGMza0VsSzdxQT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliei5zby4xLjIuMTEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiMzg4YTRmYjM2NDBmOGNjYmQxOGUxMDVkZjNhZDc0MWRjYTQyNDdlMS1kaXJ0eSIsImluc3RhbGxlZFNpemUiOjExMDU5MiwibGljZW5zZSI6IlpsaWIiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJvcmlnaW5QYWNrYWdlIjoiemxpYiIsInBhY2thZ2UiOiJ6bGliIiwicHVsbENoZWNrc3VtIjoiUTFXQm8rNTdKbGRsVmUwaVZ0Mm44SVA2K3ZOR0U9IiwicHVsbERlcGVuZGVuY2llcyI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSIsInNpemUiOjUxNzQyLCJ1cmwiOiJodHRwczovL3psaWIubmV0LyIsInZlcnNpb24iOiIxLjIuMTEtcjMifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiemxpYiIsInB1cmwiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjExLXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjIuMTEtcjMifV0sImRlc2NyaXB0b3IiOnsiY29uZmlndXJhdGlvbiI6eyJhbmNob3JlIjp7ImRvY2tlcmZpbGUiOiIiLCJob3N0IjoiIiwiaW1wb3J0LXRpbWVvdXQiOjMwLCJvdmVyd3JpdGUtZXhpc3RpbmctaW1hZ2UiOmZhbHNlLCJwYXRoIjoiIn0sImF0dGVzdCI6eyJrZXkiOiJjb3NpZ24ua2V5In0sImNoZWNrLWZvci1hcHAtdXBkYXRlIjp0cnVlLCJjb25maWdQYXRoIjoiIiwiZGV2Ijp7InByb2ZpbGUtY3B1IjpmYWxzZSwicHJvZmlsZS1tZW0iOmZhbHNlfSwiZXhjbHVkZSI6W10sImZpbGUiOiIiLCJmaWxlLWNsYXNzaWZpY2F0aW9uIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9fSwiZmlsZS1jb250ZW50cyI6eyJjYXRhbG9nZXIiOnsiZW5hYmxlZCI6ZmFsc2UsInNjb3BlIjoiU3F1YXNoZWQifSwiZ2xvYnMiOltdLCJza2lwLWZpbGVzLWFib3ZlLXNpemUiOjEwNDg1NzZ9LCJmaWxlLW1ldGFkYXRhIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9LCJkaWdlc3RzIjpbInNoYTI1NiJdfSwibG9nIjp7ImZpbGUtbG9jYXRpb24iOiIiLCJsZXZlbCI6ImVycm9yIiwic3RydWN0dXJlZCI6ZmFsc2V9LCJvdXRwdXQiOlsianNvbiJdLCJwYWNrYWdlIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjp0cnVlLCJzY29wZSI6IlNxdWFzaGVkIn0sInNlYXJjaC1pbmRleGVkLWFyY2hpdmVzIjp0cnVlLCJzZWFyY2gtdW5pbmRleGVkLWFyY2hpdmVzIjpmYWxzZX0sInBsYXRmb3JtIjoiIiwicXVpZXQiOmZhbHNlLCJyZWdpc3RyeSI6eyJhdXRoIjpbXSwiaW5zZWN1cmUtc2tpcC10bHMtdmVyaWZ5IjpmYWxzZSwiaW5zZWN1cmUtdXNlLWh0dHAiOmZhbHNlfSwic2VjcmV0cyI6eyJhZGRpdGlvbmFsLXBhdHRlcm5zIjp7fSwiY2F0YWxvZ2VyIjp7ImVuYWJsZWQiOmZhbHNlLCJzY29wZSI6IkFsbExheWVycyJ9LCJleGNsdWRlLXBhdHRlcm4tbmFtZXMiOltdLCJyZXZlYWwtdmFsdWVzIjpmYWxzZSwic2tpcC1maWxlcy1hYm92ZS1zaXplIjoxMDQ4NTc2fX0sIm5hbWUiOiJzeWZ0IiwidmVyc2lvbiI6IjAuNDIuMSJ9LCJkaXN0cm8iOnsiYnVnUmVwb3J0VVJMIjoiaHR0cHM6Ly9idWdzLmFscGluZWxpbnV4Lm9yZy8iLCJob21lVVJMIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcvIiwiaWQiOiJhbHBpbmUiLCJuYW1lIjoiQWxwaW5lIExpbnV4IiwicHJldHR5TmFtZSI6IkFscGluZSBMaW51eCB2My4xNSIsInZlcnNpb25JRCI6IjMuMTUuMiJ9LCJmaWxlcyI6W3siaWQiOiI2YzczMTQ0ZWE5ZWY0ZmI5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9iaW4vYnVzeWJveCJ9fSx7ImlkIjoiZTQxZGFkOGE2MWZmODNiNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiYzliZWM1M2MwYTNlMjEwNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYTVkZmQyOTE0ODFjNjllMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiYzE1ZjkyZGRjNzdjMWFlNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiZDBjMjQ1M2Y5OGIzMTFkOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QifX0seyJpZCI6ImM3OTVkM2Y3ZWJkNjRlODQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9mc3RhYiJ9fSx7ImlkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2dyb3VwIn19LHsiaWQiOiJkZjIyZWQxYTMxZmRiYjY4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvaG9zdG5hbWUifX0seyJpZCI6ImU4NWFmMTRkNTAzMjU3NDQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9ob3N0cyJ9fSx7ImlkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2luaXR0YWIifX0seyJpZCI6IjlmYTk0ZWE4ZTg3NGVlNDkiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZC9hY3BpZCJ9fSx7ImlkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn19LHsiaWQiOiJkYmU3MTZlMDVmOTExZmY5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9ibGFja2xpc3QuY29uZiJ9fSx7ImlkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvaTM4Ni5jb25mIn19LHsiaWQiOiI5YTM1NzZlNGFhZGI4MmYxIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9fSx7ImlkIjoiNWEyNWQwZGJlODBhYjI0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9fSx7ImlkIjoiNTU1OGY3NjAzOGQxYzk3ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vdGQifX0seyJpZCI6IjU1OWE5ZDUxZTMzZDI0MTAiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQvZGFkIn19LHsiaWQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcGFzc3dkIn19LHsiaWQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9fSx7ImlkIjoiMzg3MTJhZGVhZWRiNzMxNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9SRUFETUUifX0seyJpZCI6IjE0ZGQ5ODVkY2U0Y2Q1YTUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvY29sb3JfcHJvbXB0LnNoLmRpc2FibGVkIn19LHsiaWQiOiI1OTBjYzI2MzU4N2UyYzlkIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9fSx7ImlkIjoiMjZhMjBmMTA2ZmEzMjc4MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb3RvY29scyJ9fSx7ImlkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlY3VyZXR0eSJ9fSx7ImlkIjoiZjI5YWYyNDc1NTBkY2FjOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlcnZpY2VzIn19LHsiaWQiOiI0YTZkNmUwZWE5NTc1MWI3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hhZG93In19LHsiaWQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hlbGxzIn19LHsiaWQiOiI1YTg0NzZiZDZmNWExM2JmIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc3NsL2NlcnRzL2NhLWNlcnRpZmljYXRlcy5jcnQifX0seyJpZCI6ImJjNjYzNjJiMjVjNjQzNGEiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY3RfbG9nX2xpc3QuY25mLmRpc3QifX0seyJpZCI6ImRjNWNiYjc4Y2M3NDBmYmUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYifX0seyJpZCI6ImRlYzIzOWI1NjVkMTliNjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYuZGlzdCJ9fSx7ImlkIjoiYjhmMWU0YTYyYjIxNjYzYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3N5c2N0bC5jb25mIn19LHsiaWQiOiI2YzZmOThjODU3YTY5MWNjIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvdWRoY3BkLmNvbmYifX0seyJpZCI6IjRhY2E2ZDE1ZGYwOWRhMWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9sZC1tdXNsLXg4Nl82NC5zby4xIn19LHsiaWQiOiI4NTI0ZGNmMDI5MzZkODE3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliYXBrLnNvLjMuMTIuMCJ9fSx7ImlkIjoiYmExYWQxNTM5NGM1MzkyMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2xpYmNyeXB0by5zby4xLjEifX0seyJpZCI6IjZiMWM5YTE4Y2FiYWYzZWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9saWJzc2wuc28uMS4xIn19LHsiaWQiOiIxM2ExYWJkMWQyZDM0YTU3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliei5zby4xLjIuMTEifX0seyJpZCI6IjQ1NmE3MDJmMTY3MzNiMjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9zeXNjdGwuZC8wMC1hbHBpbmUuY29uZiJ9fSx7ImlkIjoiMmM2MjRmYmYzOWNhN2Q2NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvc2Jpbi9hcGsifX0seyJpZCI6IjIzNzAyNmQxMzRiODRkNmYiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbGRjb25maWcifX0seyJpZCI6IjE0ZTNmNDFhNzdiNDI4MDciLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbWttbnRkaXJzIn19LHsiaWQiOiIzY2E3Y2JmZDU5MzBkOTBiIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2dldGNvbmYifX0seyJpZCI6IjlhY2M0NmY5ZjlkMjQwNTgiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0ZW50In19LHsiaWQiOiIxNjJmMzI0MDM2NTYxNjQzIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2ljb252In19LHsiaWQiOiJhMjliNDViNWMwMmQwMDA3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2xkZCJ9fSx7ImlkIjoiMmFmNDEyYjEwZmYyYzVmYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2Jpbi9zY2FuZWxmIn19LHsiaWQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL3NzbF9jbGllbnQifX0seyJpZCI6ImU2MDU0MmZhMzNkYWViMDUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvYWZhbGcuc28ifX0seyJpZCI6IjJkMjRjNDBhODhiNGMyZWMiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvY2FwaS5zbyJ9fSx7ImlkIjoiNWRiMzhkZGNlODkxMDVlNyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9wYWRsb2NrLnNvIn19LHsiaWQiOiIyYjAyZjY3OTBjNmVmN2U4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvbGliL2xpYnRscy5zby4yLjAuMyJ9fSx7ImlkIjoiYTJiZjQxNDAyY2MwNDMxNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiZGI0NjJiZDRjZTliYWVjNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYzVmMTI2MmE2NmJmMDQ4YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiJ9fSx7ImlkIjoiZWUzYWQ5YzBhNGY1NGU5MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiOTJkMzU4ZTcyNTZmMGI3YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTgxOTlkY2MucnNhLnB1YiJ9fSx7ImlkIjoiNThmYmViYTc3NGViMmE1NiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThjYmI0NzYucnNhLnB1YiJ9fSx7ImlkIjoiZmM4MjgxZjNiZmQ1NzFlNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiJ9fSx7ImlkIjoiZGMxZTA0M2M2NTFkZmI0YyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNWU2OWNhNTAucnNhLnB1YiJ9fSx7ImlkIjoiNzZiNWFhODA1ZGMxYjE4ZiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiJ9fSx7ImlkIjoiYTAwZjcxNjgxOWZhYTgxOSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiYTNlNzZmMjE5MzNiNTFjNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMTFkNGU3NjhjMmViNzYyZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YTk3MjQucnNhLnB1YiJ9fSx7ImlkIjoiNTIyYTYwMmU5YzEwMGU2MiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiJ9fSx7ImlkIjoiYWM3YTBmMjFkZWMwM2ExYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWMzYmMucnNhLnB1YiJ9fSx7ImlkIjoiMzkxMTQ1ZjBlZmQzOWI5NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiJ9fSx7ImlkIjoiMjk1YzI2M2IzZDFjODFkMiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiJ9fSx7ImlkIjoiNGE5NmY4YzRjZjM0Njc1OSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2ZGIzMGQucnNhLnB1YiJ9fSx7ImlkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYy9kZWZhdWx0LnNjcmlwdCJ9fV0sInNjaGVtYSI6eyJ1cmwiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYW5jaG9yZS9zeWZ0L21haW4vc2NoZW1hL2pzb24vc2NoZW1hLTMuMi4xLmpzb24iLCJ2ZXJzaW9uIjoiMy4yLjEifSwic291cmNlIjp7InRhcmdldCI6eyJhcmNoaXRlY3R1cmUiOiIiLCJjb25maWciOiJleUpoY21Ob2FYUmxZM1IxY21VaU9pSmhiV1EyTkNJc0ltTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJaUlzSWtSdmJXRnBibTVoYldVaU9pSWlMQ0pWYzJWeUlqb2lJaXdpUVhSMFlXTm9VM1JrYVc0aU9tWmhiSE5sTENKQmRIUmhZMmhUZEdSdmRYUWlPbVpoYkhObExDSkJkSFJoWTJoVGRHUmxjbklpT21aaGJITmxMQ0pVZEhraU9tWmhiSE5sTENKUGNHVnVVM1JrYVc0aU9tWmhiSE5sTENKVGRHUnBiazl1WTJVaU9tWmhiSE5sTENKRmJuWWlPbHNpVUVGVVNEMHZkWE55TDJ4dlkyRnNMM05pYVc0NkwzVnpjaTlzYjJOaGJDOWlhVzQ2TDNWemNpOXpZbWx1T2k5MWMzSXZZbWx1T2k5elltbHVPaTlpYVc0aVhTd2lRMjFrSWpwYklpOWlhVzR2YzJnaVhTd2lTVzFoWjJVaU9pSnphR0V5TlRZNlpUaGxNRE5oWldVMlpqRTJaRGhsWW1GbU9UVmhORGs0TXpBd01UTTNNMkkwTkRjNU1HTmpPR1EwTW1NMVpUZzBZV1kwWmpObU9ERTBaVFF4TWpFeVppSXNJbFp2YkhWdFpYTWlPbTUxYkd3c0lsZHZjbXRwYm1kRWFYSWlPaUlpTENKRmJuUnllWEJ2YVc1MElqcHVkV3hzTENKUGJrSjFhV3hrSWpwdWRXeHNMQ0pNWVdKbGJITWlPbTUxYkd4OUxDSmpiMjUwWVdsdVpYSWlPaUprTXpJMk1qQXlOR0ZqTUdZd09EYzVNR1ppTWpNMlpXRXpNbU0xT1dGak1XWmtNamMzTldVeU1qYzFNR1UxWXpCaVlXWTFaV0UxWXpBeE1qTXhOR0ppSWl3aVkyOXVkR0ZwYm1WeVgyTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJbVF6TWpZeU1ESTBZV013WmlJc0lrUnZiV0ZwYm01aGJXVWlPaUlpTENKVmMyVnlJam9pSWl3aVFYUjBZV05vVTNSa2FXNGlPbVpoYkhObExDSkJkSFJoWTJoVGRHUnZkWFFpT21aaGJITmxMQ0pCZEhSaFkyaFRkR1JsY25JaU9tWmhiSE5sTENKVWRIa2lPbVpoYkhObExDSlBjR1Z1VTNSa2FXNGlPbVpoYkhObExDSlRkR1JwYms5dVkyVWlPbVpoYkhObExDSkZibllpT2xzaVVFRlVTRDB2ZFhOeUwyeHZZMkZzTDNOaWFXNDZMM1Z6Y2k5c2IyTmhiQzlpYVc0NkwzVnpjaTl6WW1sdU9pOTFjM0l2WW1sdU9pOXpZbWx1T2k5aWFXNGlYU3dpUTIxa0lqcGJJaTlpYVc0dmMyZ2lMQ0l0WXlJc0lpTW9ibTl3S1NBaUxDSkRUVVFnVzF3aUwySnBiaTl6YUZ3aVhTSmRMQ0pKYldGblpTSTZJbk5vWVRJMU5qcGxPR1V3TTJGbFpUWm1NVFprT0dWaVlXWTVOV0UwT1Rnek1EQXhNemN6WWpRME56a3dZMk00WkRReVl6VmxPRFJoWmpSbU0yWTRNVFJsTkRFeU1USm1JaXdpVm05c2RXMWxjeUk2Ym5Wc2JDd2lWMjl5YTJsdVowUnBjaUk2SWlJc0lrVnVkSEo1Y0c5cGJuUWlPbTUxYkd3c0lrOXVRblZwYkdRaU9tNTFiR3dzSWt4aFltVnNjeUk2ZTMxOUxDSmpjbVZoZEdWa0lqb2lNakF5TWkwd015MHlNMVF4TlRveU1Ub3lNUzR4TVRrNU1EY3pNVGxhSWl3aVpHOWphMlZ5WDNabGNuTnBiMjRpT2lJeU1DNHhNQzR4TWlJc0ltaHBjM1J2Y25raU9sdDdJbU55WldGMFpXUWlPaUl5TURJeUxUQXpMVEl6VkRFMU9qSXhPakl4TGpBeU16SXpOVGM0TmxvaUxDSmpjbVZoZEdWa1gySjVJam9pTDJKcGJpOXphQ0F0WXlBaktHNXZjQ2tnUVVSRUlHWnBiR1U2TnpNNE5tRmtPRGt6TmpjeU1EQTNZMk5oTW1RM00yTmxZekU0TmpKa05UZ3lZVFk1WkRVNE1XTmhNV1F4TlRWa05EVTVPV05pTW1GaE5UUmtOVFE1T0NCcGJpQXZJQ0o5TEhzaVkzSmxZWFJsWkNJNklqSXdNakl0TURNdE1qTlVNVFU2TWpFNk1qRXVNVEU1T1RBM016RTVXaUlzSW1OeVpXRjBaV1JmWW5raU9pSXZZbWx1TDNOb0lDMWpJQ01vYm05d0tTQWdRMDFFSUZ0Y0lpOWlhVzR2YzJoY0lsMGlMQ0psYlhCMGVWOXNZWGxsY2lJNmRISjFaWDFkTENKdmN5STZJbXhwYm5WNElpd2ljbTl2ZEdaeklqcDdJblI1Y0dVaU9pSnNZWGxsY25NaUxDSmthV1ptWDJsa2N5STZXeUp6YUdFeU5UWTZabVkzTmpoaE1UUXhNMkpoTVRBNU16QTRZekJrT0RrM1ptWmhOVFUxWlRBMU1tRTBObVF5WTJZME56RXhOemhtTURBeFlqZ3lOV0kwWXpJeFpqTTFOQ0pkZlgwPSIsImltYWdlSUQiOiJzaGEyNTY6OWM4NDJhYzQ5YTM5ZmU0MmU3MWE2MjMxODNmZTdmYjdjNzU5ZDU5MDI5ZTlhOGU3ODUxYzM1N2M3ZDhhODZmOCIsImltYWdlU2l6ZSI6NTU3MDE0NywibGF5ZXJzIjpbeyJkaWdlc3QiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1NTcwMTQ3fV0sIm1hbmlmZXN0IjoiZXdvZ0lDQWljMk5vWlcxaFZtVnljMmx2YmlJNklESXNDaUFnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1a2FYTjBjbWxpZFhScGIyNHViV0Z1YVdabGMzUXVkaklyYW5OdmJpSXNDaUFnSUNKamIyNW1hV2NpT2lCN0NpQWdJQ0FnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1amIyNTBZV2x1WlhJdWFXMWhaMlV1ZGpFcmFuTnZiaUlzQ2lBZ0lDQWdJQ0p6YVhwbElqb2dNVFEzTWl3S0lDQWdJQ0FnSW1ScFoyVnpkQ0k2SUNKemFHRXlOVFk2T1dNNE5ESmhZelE1WVRNNVptVTBNbVUzTVdFMk1qTXhPRE5tWlRkbVlqZGpOelU1WkRVNU1ESTVaVGxoT0dVM09EVXhZek0xTjJNM1pEaGhPRFptT0NJS0lDQWdmU3dLSUNBZ0lteGhlV1Z5Y3lJNklGc0tJQ0FnSUNBZ2V3b2dJQ0FnSUNBZ0lDQWliV1ZrYVdGVWVYQmxJam9nSW1Gd2NHeHBZMkYwYVc5dUwzWnVaQzVrYjJOclpYSXVhVzFoWjJVdWNtOXZkR1p6TG1ScFptWXVkR0Z5TG1kNmFYQWlMQW9nSUNBZ0lDQWdJQ0FpYzJsNlpTSTZJREk0TVRJMk9Ea3NDaUFnSUNBZ0lDQWdJQ0prYVdkbGMzUWlPaUFpYzJoaE1qVTJPak5oWVRSa01HSmlaR1V4T1RKaVptRmlZVGMxWmpKa01USTBaRGhqWmpKbE5tUmxORFV5WVdVd00yVTFOV1ExTkRFd05XVTBObUl3Tm1WaU9ERXlOMlVpQ2lBZ0lDQWdJSDBLSUNBZ1hRcDkiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo3M2MxNTU2OTZmZTY1YjY4Njk2ZTZlYTI0MDg4NjkzNTQ2YWM0NjhiM2UxNDU0MmYyM2YwZWZiZGUyODljYzk3IiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsIm9zIjoiIiwicmVwb0RpZ2VzdHMiOlsiaW5kZXguZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lQHNoYTI1Njo2YWYxYjExYmJiMTdmNGMzMTFlMjY5ZGI2NTMwZTRkYTI3MzgyNjJhZjVmZDkwNjRjY2RmMTA5Yjc2NTg2MGZiIl0sInRhZ3MiOltdLCJ1c2VySW5wdXQiOiJhbHBpbmU6bGF0ZXN0In0sInR5cGUiOiJpbWFnZSJ9fX0=","signatures":[{"keyid":"","sig":"MEUCIQDBtal1MWSsNl8U1neDA1Ujec8HvbJ5T4tWtuFNY7OkrgIgFY+wklqhg6Y/HhivWlMmcA593sx6pNnusAqTLlNtIP0="}]} ================================================ FILE: grype/pkg/testdata/alpine.cdx.att.json ================================================ {"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6IiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI0ZWRiZDJiZWI1Zjc4YjEwMTQwMjhmNGZiYjk5ZjMyMzdkOTU2MTEwMGI2ODgxYWFiYmY1YWNjZTJjNGY5NDU0In19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsImNvbXBvbmVudHMiOlt7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2FscGluZS1iYXNlbGF5b3V0QDMuMi4wLXIxOD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTlmNTI3MjEzZjRkMmE4NzMiLCJjcGUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0LmFscGluZWxpbnV4Lm9yZy9jZ2l0L2Fwb3J0cy90cmVlL21haW4vYWxwaW5lLWJhc2VsYXlvdXQifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNDEzNjk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUtYmFzZWxheW91dCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMUV5bVM2ckFnbUdzN1hZaHFkeUVvaVdnRVo2QT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Ii9iaW4vc2ggc286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyMTEwMSJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUtYmFzZWxheW91dEAzLjIuMC1yMTg/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUtYmFzZWxheW91dFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4yLjAtcjE4In0seyJib20tcmVmIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUta2V5c1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xYTcyY2EzYjg4ZTFiNjdlIiwiY3BlIjoiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJQdWJsaWMga2V5cyBmb3IgQWxwaW5lIExpbnV4IHBhY2thZ2VzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6ImFscGluZS1rZXlzIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfa2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYWFiNjhmOGM5YWI0MzRhNDY3MTBkZThlMTJmYjMyMDZlMjkzMGE1OSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTU5NzQ0In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUta2V5cyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWtERjJzdEtvM2UvUnVtbEE4WnJSZkN3ZFN2OD0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjEzMzYyIn1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2FscGluZS1rZXlzQDIuNC1yMT9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1rZXlzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyLjQtcjEifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MWM2ZTA1N2M2OTY1YmRkNiIsImNwZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBbHBpbmUgUGFja2FnZSBLZWVwZXIgLSBwYWNrYWdlIG1hbmFnZXIgZm9yIGFscGluZSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0bGFiLmFscGluZWxpbnV4Lm9yZy9hbHBpbmUvYXBrLXRvb2xzIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkdQTC0yLjAtb25seSJ9fV0sIm5hbWUiOiJhcGstdG9vbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwa190b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YXBrX3Rvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGs6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwazphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6bGF5ZXJJRCIsInZhbHVlIjoic2hhMjU2OjRmYzI0MmQ1ODI4NTY5OWVjYTA1ZGIzY2M3YzcxMjJhMmI4ZTAxNGQ5NDgxZjMyM2JkOTI3N2JhYWNmYTA2MjgifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6cGF0aCIsInZhbHVlIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpnaXRDb21taXRPZkFwa1BvcnQiLCJ2YWx1ZSI6IjFhYzNjMWJiMjllZWZmMDgzYzYyMWNmNmIyN2FkMTJhYjkzY2I3M2EifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmluc3RhbGxlZFNpemUiLCJ2YWx1ZSI6IjMxMTI5NiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoiYXBrLXRvb2xzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExM2ZQZCtGUlhhTHd5TmtsVm4rcXVGV0R5a25NPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoibXVzbFx1MDAzZT0xLjIgY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIHNvOmxpYnouc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwMzc3In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNCIsInR5cGUiOiJsaWJyYXJ5IiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWE1MjcwMjYxMmFhMTZkZTMiLCJjcGUiOiJjcGU6Mi4zOmE6YnVzeWJveDpidXN5Ym94OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTaXplIG9wdGltaXplZCB0b29sYm94IG9mIG1hbnkgY29tbW9uIFVOSVggdXRpbGl0aWVzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImJ1c3lib3giLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQ2MTc2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJidXN5Ym94In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExTFV5UEtJS3pVSzZ2cEpjQmorTzQwNGVmYXdnPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiI1MDA2NzgifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2NhLWNlcnRpZmljYXRlcy1idW5kbGVAMjAyMTEyMjAtcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1jYS1jZXJ0aWZpY2F0ZXNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MmM0NTIyMjkyM2QzMGZjNSIsImNwZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiUHJlIGdlbmVyYXRlZCBidW5kbGUgb2YgTW96aWxsYSBjZXJ0aWZpY2F0ZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy9hYm91dC9nb3Zlcm5hbmNlL3BvbGljaWVzL3NlY3VyaXR5LWdyb3VwL2NlcnRzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJNUEwtMi4wIn19LHsibGljZW5zZSI6eyJpZCI6Ik1JVCJ9fV0sIm5hbWUiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXM6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2E6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3MDliNzBiY2I3MjczOGNmZWRjNTEwYmJhMDgxNDFiMDEyMDM4MTY3In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIyMjExODQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImNhLWNlcnRpZmljYXRlcyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMVNWQVd1V0hkUEh2YkJoTFRrQVo2MC8xV3NtST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjExOTc0OCJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9jYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlQDIwMjExMjIwLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09Y2EtY2VydGlmaWNhdGVzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWU4N2E3OWZkYWVjYWFiZDIiLCJjcGUiOiJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6Ik1ldGEgcGFja2FnZSB0byBwdWxsIGluIGNvcnJlY3QgbGliYyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vYWxwaW5lbGludXgub3JnIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkJTRC0yLUNsYXVzZSJ9fSx7ImxpY2Vuc2UiOnsiaWQiOiJCU0QtMy1DbGF1c2UifX1dLCJuYW1lIjoibGliYy11dGlscyIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmMtdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI2MDQyNDEzM2JlMmU3OWJiZmVmZjNkNTgxNDdhMjI4ODZmODE3Y2UyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiI0MDk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJsaWJjLWRldiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWVZM2o2N1YvUGlqMENBZ0hScE5mSVRvSmx5ST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Im11c2wtdXRpbHMifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjE0ODUifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliY3J5cHRvMS4xQDEuMS4xbi1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW9wZW5zc2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MTdjNmZiYjBjYWZiYmU1NyIsImNwZSI6ImNwZToyLjM6YTpsaWJjcnlwdG8xLjE6bGliY3J5cHRvMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJDcnlwdG8gbGlicmFyeSBmcm9tIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJjcnlwdG8xLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjc0MDIyNCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoib3BlbnNzbCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMXJBc0xjYlk5NlQrVHFvdTBNSDB5UFExMWhHUT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwODIyOCJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnJldGxzQDMuMy40LXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09bGlicmV0bHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9NmQwNjU4YzJiNzIzN2VhMiIsImNwZSI6ImNwZToyLjM6YTpsaWJyZXRsczpsaWJyZXRsczozLjMuNC1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJwb3J0IG9mIGxpYnRscyBmcm9tIGxpYnJlc3NsIHRvIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL2dpdC5jYXVzYWwuYWdlbmN5L2xpYnJldGxzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJJU0MifX1dLCJuYW1lIjoibGlicmV0bHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiOTFjN2E5ZjNhYTI5NmI2ZDQ2MmM1NjM0ZTc2NThlYmRiZmY2NWJiOSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiODYwMTYifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImxpYnJldGxzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExWjkvdjVVVnNSUmtyWU5kcTNwakZBYkN1Z1U4PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyOTE4NSJ9XSwicHVibGlzaGVyIjoiQXJpYWRuZSBDb25pbGwgXHUwMDNjYXJpYWRuZUBkZXJlZmVyZW5jZWQub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGlicmV0bHNAMy4zLjQtcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1saWJyZXRsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4zLjQtcjMifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTRiMTA2ZTZjM2ZkNzRjMWMiLCJjcGUiOiJjcGU6Mi4zOmE6bGlic3NsMS4xOmxpYnNzbDEuMToxLjEuMW4tcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiU1NMIHNoYXJlZCBsaWJyYXJpZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJzc2wxLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNTQwNjcyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJvcGVuc3NsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExL0taMDBxREhXWjVjajNBV0cvRFBkQUNSTllJPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjIxMzIwOSJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTIwZGMyMGNiYjZkYmVhNiIsImNwZSI6ImNwZToyLjM6YTptdXNsOm11c2w6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNjIyNTkyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExRGViMGpOeXRrcmpQVzROL2VLTFo0M0J3T2x3PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzgzMTUyIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbXVzbC11dGlsc0AxLjIuMi1yNz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW11c2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MzVjMzY4MDU3N2ZhZTBkZiIsImNwZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wtdXRpbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTQzMzYwIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExUDUwY2ZKaVNzSG9xc1lSVHlPRU9sSmlMbjNvPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic2NhbmVsZiBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjM2NzIzIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2wtdXRpbHNAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1mMmQ0MjYzNzIzNTY2MDJkIiwiY3BlIjoiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTY2FuIEVMRiBiaW5hcmllcyBmb3Igc3R1ZmYiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3dpa2kuZ2VudG9vLm9yZy93aWtpL0hhcmRlbmVkL1BhWF9VdGlsaXRpZXMifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNjYW5lbGYiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQyMDgifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InBheC11dGlscyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMTEvZFpEa1VJY0tUM2xuSENOcHN4dGJzSE5Kbz0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzY4MzAifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3NzbF9jbGllbnRAMS4zNC4xLXI1P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YnVzeWJveFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xZTE4MTJkZWI2NjY5MmM1IiwiY3BlIjoiY3BlOjIuMzphOnNzbC1jbGllbnQ6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiRVh0ZXJuYWwgc3NsX2NsaWVudCBmb3IgYnVzeWJveCB3Z2V0IiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNzbF9jbGllbnQiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbF9jbGllbnQ6MS4zNC4xLXI1Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6c3NsX2NsaWVudDpzc2wtY2xpZW50OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjg2NzIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImJ1c3lib3gifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFMUXBhVFlaVHpSL2tnS3ZSd0x1WThkRUZZUDQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGlidGxzLnNvLjIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjQ3MTYifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc3NsX2NsaWVudEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1iMzk5MDhlNzI5NzQ2MDkiLCJjcGUiOiJjcGU6Mi4zOmE6emxpYjp6bGliOjEuMi4xMi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBIGNvbXByZXNzaW9uL2RlY29tcHJlc3Npb24gTGlicmFyeSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vemxpYi5uZXQvIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IlpsaWIifX1dLCJuYW1lIjoiemxpYiIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3NDE0ODgwODY3OWY0N2FkOTZkYzk5ZTgzZWY3M2FjZmRlZWMxNjQyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIxMTA1OTIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InpsaWIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFIa3AyekgyYXlBV25RNzNrMFJkMjcwY1FCQjQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjUzNDg4In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4yLjEyLXIwIn0seyJkZXNjcmlwdGlvbiI6IkFscGluZSBMaW51eCB2My4xNSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6Imlzc3VlLXRyYWNrZXIiLCJ1cmwiOiJodHRwczovL2J1Z3MuYWxwaW5lbGludXgub3JnLyJ9LHsidHlwZSI6IndlYnNpdGUiLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZy8ifV0sIm5hbWUiOiJhbHBpbmUiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpkaXN0cm86aWQiLCJ2YWx1ZSI6ImFscGluZSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnByZXR0eU5hbWUiLCJ2YWx1ZSI6IkFscGluZSBMaW51eCB2My4xNSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnZlcnNpb25JRCIsInZhbHVlIjoiMy4xNS40In1dLCJzd2lkIjp7Im5hbWUiOiJhbHBpbmUiLCJ0YWdJZCI6ImFscGluZSIsInZlcnNpb24iOiIzLjE1LjQifSwidHlwZSI6Im9wZXJhdGluZy1zeXN0ZW0iLCJ2ZXJzaW9uIjoiMy4xNS40In1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6ImZjNjM4NjY1ZDQwNDdlYTEiLCJuYW1lIjoiYWxwaW5lOmxhdGVzdCIsInR5cGUiOiJjb250YWluZXIiLCJ2ZXJzaW9uIjoic2hhMjU2OmE3NzdjOWM2NmJhMTc3Y2NmZWEyM2YyYTIxNmZmNjcyMWU3OGE2NjJjZDE3MDE5NDg4YzQxNzEzNTI5OWNkODkifSwidGltZXN0YW1wIjoiMjAyMi0wNC0xMVQxNzoxMTo0MS0wNzowMCIsInRvb2xzIjpbeyJuYW1lIjoic3lmdCIsInZlbmRvciI6ImFuY2hvcmUiLCJ2ZXJzaW9uIjoiMC40My4yIn1dfSwic2VyaWFsTnVtYmVyIjoidXJuOnV1aWQ6ZWQ5MjQyNmYtYTkxNi00NDljLTgzYjgtZDcwODYwMTM3NzczIiwic3BlY1ZlcnNpb24iOiIxLjQiLCJ2ZXJzaW9uIjoxfX0=","signatures":[{"keyid":"","sig":"MEQCIDq1ltJFSy/cIzUSQmnqcP4ttqBY6vc92Nld45QY12GoAiBbjJQmixgSc6Hm5fClMXZnoyWbZJjKouQDzX5bvtWd0w=="}]} ================================================ FILE: grype/pkg/testdata/another_cosign.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFkdSiEHXuVIQMJWLeRbj+xuAIdzB YNPLm67ahl7GBcPYWirZfssklnwDldY8TLbK4igxT7YisGPMLGyiJsvvhg== -----END PUBLIC KEY----- ================================================ FILE: grype/pkg/testdata/bad-sbom.json ================================================ {} ================================================ FILE: grype/pkg/testdata/cosign.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB ZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg== -----END PUBLIC KEY----- ================================================ FILE: grype/pkg/testdata/cosign_broken.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB ZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg=1 -----END PUBLIC KEY----- ================================================ FILE: grype/pkg/testdata/image-simple/Dockerfile ================================================ # Note: changes to this file will necessitate updating test values. alternately, make a new image fixture instead of editing this one. FROM scratch ADD package.json / ADD target /target ================================================ FILE: grype/pkg/testdata/image-simple/package.json ================================================ { "name": "top-level-package", "version": "5.19.4", "dependencies": { "left-pad": "1.3.0" } } ================================================ FILE: grype/pkg/testdata/image-simple/target/nested/package.json ================================================ { "name": "nested-package", "version": "2.9.35", "dependencies": { "lodash": "4.17.21" } } ================================================ FILE: grype/pkg/testdata/invalid.json ================================================ {} ================================================ FILE: grype/pkg/testdata/purl/different-os.txt ================================================ pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3 pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2 ================================================ FILE: grype/pkg/testdata/purl/empty.json ================================================ ================================================ FILE: grype/pkg/testdata/purl/homogeneous-os.txt ================================================ pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3 pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3 ================================================ FILE: grype/pkg/testdata/purl/invalid-cpe.txt ================================================ pkg:deb/debian/curl@7.50.3-1?cpes=invalid ================================================ FILE: grype/pkg/testdata/purl/invalid-purl.txt ================================================ invalid ================================================ FILE: grype/pkg/testdata/purl/valid-purl.txt ================================================ pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit pkg:maven/org.apache.ant/ant@1.10.8 pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1 ================================================ FILE: grype/pkg/testdata/purl/valid-rhel-9+eus.txt ================================================ pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4+eus ================================================ FILE: grype/pkg/testdata/purl/valid-rhel-9.txt ================================================ pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4 ================================================ FILE: grype/pkg/testdata/sbom-with-intoto-string.json ================================================ { "artifacts": [ { "name": "gcc-10-base", "version": "10.2.0-5ubuntu1~20.04", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/gcc-10-base:amd64.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/gcc-10-base/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:gcc-10-base:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*", "cpe:2.3:a:*:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/gcc-10-base@10.2.0-5ubuntu1~20.04?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "gcc-10-base", "source": "gcc-10", "version": "10.2.0-5ubuntu1~20.04", "sourceVersion": "", "architecture": "amd64", "maintainer": "application/vnd.in-toto+json this is here to make grype think it is an attestation", "installedSize": 260, "files": [ { "path": "/usr/share/doc/gcc-10-base/README.Debian.amd64.gz", "md5": "3c03902e06eef5dcfe3005376c23a120" }, { "path": "/usr/share/doc/gcc-10-base/TODO.Debian", "md5": "8afe308ec72834f3c24b209fbc4d149e" }, { "path": "/usr/share/doc/gcc-10-base/changelog.Debian.gz", "md5": "0e3cbc1152a18bddf7c24fe3913866c6" }, { "path": "/usr/share/doc/gcc-10-base/copyright", "md5": "a80ca2e181b9eecc3e4d373fd7ca59f2" } ] } }, { "name": "hostname", "version": "3.23", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/hostname.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/hostname/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:hostname:hostname:3.23:*:*:*:*:*:*:*", "cpe:2.3:a:*:hostname:3.23:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/hostname@3.23?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "hostname", "source": "", "version": "3.23", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Developers ", "installedSize": 54, "files": [ { "path": "/bin/hostname", "md5": "1ce73d718e3dccc1aaa7bce6ae2ef0a7" }, { "path": "/usr/share/doc/hostname/changelog.gz", "md5": "087a3eabd7427692c216a5d7a4341127" }, { "path": "/usr/share/doc/hostname/copyright", "md5": "460b6a1df2db2b5e80f05a44ec21c62f" }, { "path": "/usr/share/man/man1/hostname.1.gz", "md5": "62e6be6a928b4b9f2a985778fee171fd" } ] } }, { "name": "libacl1", "version": "2.2.53-6", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/libacl1:amd64.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/libacl1/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [ "GPL-2+", "LGPL-2+" ], "language": "", "cpes": [ "cpe:2.3:a:libacl1:libacl1:2.2.53-6:*:*:*:*:*:*:*", "cpe:2.3:a:*:libacl1:2.2.53-6:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/libacl1@2.2.53-6?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "libacl1", "source": "acl", "version": "2.2.53-6", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Developers ", "installedSize": 70, "files": [ { "path": "/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2253", "md5": "e77bf61a72656a594ef49768a7d6097b" }, { "path": "/usr/share/doc/libacl1/changelog.Debian.gz", "md5": "65de3b787d67d4755ad3ae0584aee9f2" }, { "path": "/usr/share/doc/libacl1/copyright", "md5": "40822d07cf4c0fb9ab13c2bebf51d981" } ] } }, { "name": "libattr1", "version": "1:2.4.48-5", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/libattr1:amd64.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/libattr1/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [ "GPL-2+", "LGPL-2+" ], "language": "", "cpes": [ "cpe:2.3:a:libattr1:libattr1:1:2.4.48-5:*:*:*:*:*:*:*", "cpe:2.3:a:*:libattr1:1:2.4.48-5:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/libattr1@1:2.4.48-5?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "libattr1", "source": "attr", "version": "1:2.4.48-5", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Developers ", "installedSize": 57, "files": [ { "path": "/usr/lib/x86_64-linux-gnu/libattr.so.1.1.2448", "md5": "708453da8ebde1aaca2ca69c04d4c0a8" }, { "path": "/usr/share/doc/libattr1/changelog.Debian.gz", "md5": "6465a4cda28287d4ea9979b530648ee3" }, { "path": "/usr/share/doc/libattr1/copyright", "md5": "1e0c5c8b55170890f960aad90336aaed" } ] } } ], "source": { "type": "image", "target": { "userInput": "ubuntu:20.04", "imageID": "sha256:f63181f19b2fe819156dcb068b3b5bc036820bec7014c5f77277cfa341d4cb5e", "manifestDigest": "sha256:5146935f9248826d44dfc2489abfd5f4bdfbc319a738c04dfe1ef071f228a1ac", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ "ubuntu:20.04" ], "imageSize": 72898411, "scope": "Squashed", "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009", "size": 72897593 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:dbf2c0f42a39b60301f6d3936f7f8adb59bb97d31ec11cc4a049ce81155fef89", "size": 811 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:02473afd360bd5391fa51b6e7849ce88732ae29f50f3630c3551f528eba66d1e", "size": 7 } ] } }, "distro": { "name": "ubuntu", "version": "20.04", "idLike": "debian" }, "descriptor": { "name": "syft", "version": "0.12.7" }, "schema": { "version": "1.0.1", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.1.json" } } ================================================ FILE: grype/pkg/testdata/syft-java-bad-cpes.json ================================================ { "artifacts": [ { "id": "da8f3d4c-fc48-4a79-a775-31c730ce5f97", "name": "wstx-asl", "version": "3.2.7", "type": "java-archive", "foundBy": "java-cataloger", "locations": [ { "path": "/hudson.war", "layerID": "sha256:540ce9731a52867a29db67bc9b7ead11db04918f00a6e432627d21e1160ed92b" } ], "licenses": [], "language": "java", "cpes": [ "cpe:2.3:a:http://jcp_org/en/jsr/detail?id=173:wstx_asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:http://jcp_org/en/jsr/detail?id=173:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:http://jcp-org/en/jsr/detail?id=173:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:http://jcp-org/en/jsr/detail?id=173:wstx_asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:woodstox_codehaus_org:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:woodstox-codehaus-org:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:woodstox-codehaus-org:wstx_asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:woodstox_codehaus_org:wstx_asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:wstx-asl:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:wstx-asl:wstx_asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:wstx_asl:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:wstx_asl:wstx_asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:wstx:wstx-asl:3.2.7:*:*:*:*:*:*:*", "cpe:2.3:a:wstx:wstx_asl:3.2.7:*:*:*:*:*:*:*" ], "purl": "", "metadataType": "JavaMetadata", "metadata": { "virtualPath": "/hudson.war:WEB-INF/lib/wstx-asl-3.2.7.jar", "manifest": { "main": { "Ant-Version": "Apache Ant 1.6.5", "Built-By": "tatu", "Created-By": "1.4.2_03-b02 (Sun Microsystems Inc.)", "Implementation-Title": "WoodSToX XML-processor", "Implementation-Vendor": "woodstox.codehaus.org", "Implementation-Version": "3.2.7", "Manifest-Version": "1.0", "Specification-Title": "StAX 1.0 API", "Specification-Vendor": "http://jcp.org/en/jsr/detail?id=173", "Specification-Version": "1.0" } } } } ], "artifactRelationships": [], "source": { "type": "image", "target": { "userInput": "docker.io/anchore/test_images:java", "imageID": "sha256:55125c059cf3d9e8a179c3b543deeaf8613b1aa3101515cd51a7918b857b8eea", "manifestDigest": "sha256:64dc1086a990469c65bf65c28c8d2d9062dd32aa566d045e1fe71e32e6b0d600", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ "anchore/test_images:java" ], "imageSize": 39383202, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:8ea3b23f387bedc5e3cee574742d748941443c328a75f511eb37b0d8b6164130", "size": 5608905 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:540ce9731a52867a29db67bc9b7ead11db04918f00a6e432627d21e1160ed92b", "size": 33772593 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:a155363a67edf6e6462aa47445c3d900b84cec3320b62405b495ddf65035c039", "size": 1704 } ], "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMDc5LCJkaWdlc3QiOiJzaGEyNTY6NTUxMjVjMDU5Y2YzZDllOGExNzljM2I1NDNkZWVhZjg2MTNiMWFhMzEwMTUxNWNkNTFhNzkxOGI4NTdiOGVlYSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODc5ODA4LCJkaWdlc3QiOiJzaGEyNTY6OGVhM2IyM2YzODdiZWRjNWUzY2VlNTc0NzQyZDc0ODk0MTQ0M2MzMjhhNzVmNTExZWIzN2IwZDhiNjE2NDEzMCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjMzNzc2MTI4LCJkaWdlc3QiOiJzaGEyNTY6NTQwY2U5NzMxYTUyODY3YTI5ZGI2N2JjOWI3ZWFkMTFkYjA0OTE4ZjAwYTZlNDMyNjI3ZDIxZTExNjBlZDkyYiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1NjphMTU1MzYzYTY3ZWRmNmU2NDYyYWE0NzQ0NWMzZDkwMGI4NGNlYzMzMjBiNjI0MDViNDk1ZGRmNjUwMzVjMDM5In1dfQ==", "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6NWI1MWNkNTVmNmQ5YmMyOTFkNDIzNGNmYTI2MGIyMWZmM2JkOTFkYzY2MWRlOWQ1ZmE3YzM4ZjYwNzljNGExZCIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpbXSwiTGFiZWxzIjpudWxsfSwiY29udGFpbmVyX2NvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giLCItYyIsIiMobm9wKSBDT1BZIGZpbGU6MmRkODU3MzFhNDA1NjliZmVjMDQ4ODBlNDU4OTg3NDhjNDIwMjBlMjM4NmRhMWViZjA5NDhiZjQ4YmIwZmRmZiBpbiAvICJdLCJJbWFnZSI6InNoYTI1Njo1YjUxY2Q1NWY2ZDliYzI5MWQ0MjM0Y2ZhMjYwYjIxZmYzYmQ5MWRjNjYxZGU5ZDVmYTdjMzhmNjA3OWM0YTFkIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOltdLCJMYWJlbHMiOm51bGx9LCJjcmVhdGVkIjoiMjAyMS0wNC0wMVQxNToyODoyNy4zOTE0OTA5NjFaIiwiZG9ja2VyX3ZlcnNpb24iOiIxNy4wOS4wLWNlIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjEtMDMtMzFUMjA6MTA6MDYuNjg2MzU5MTI0WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTo3MTE5MTY3YjU2ZmYxMjI4YjJmYjYzOWM3Njg5NTVjZTlkYjdhOTk5Y2Q5NDcxNzkyNDBiMjE2ZGZhNWNjYmI5IGluIC8gIn0seyJjcmVhdGVkIjoiMjAyMS0wMy0zMVQyMDoxMDowNi45MzQzNjg2MDRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApICBDTUQgW1wiL2Jpbi9zaFwiXSIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIxLTA0LTAxVDE1OjI4OjI3LjIyMTEwNTY1NVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyB3Z2V0IC1udiBodHRwczovL3JlcG8xLm1hdmVuLm9yZy9tYXZlbjIvanVuaXQvanVuaXQvNC4xMy4xL2p1bml0LTQuMTMuMS5qYXIgXHUwMDI2XHUwMDI2ICAgICB3Z2V0IC1udiBodHRwczovL2dldC5qZW5raW5zLmlvL3BsdWdpbnMvVHdpbGlvTm90aWZpZXIvMC4yLjEvVHdpbGlvTm90aWZpZXIuaHBpIFx1MDAyNlx1MDAyNiAgICAgd2dldCAtbnYgaHR0cHM6Ly91cGRhdGVzLmplbmtpbnMtY2kub3JnL2Rvd25sb2FkL3dhci8xLjM5MC9odWRzb24ud2FyIn0seyJjcmVhdGVkIjoiMjAyMS0wNC0wMVQxNToyODoyNy4zOTE0OTA5NjFaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIENPUFkgZmlsZToyZGQ4NTczMWE0MDU2OWJmZWMwNDg4MGU0NTg5ODc0OGM0MjAyMGUyMzg2ZGExZWJmMDk0OGJmNDhiYjBmZGZmIGluIC8gIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6OGVhM2IyM2YzODdiZWRjNWUzY2VlNTc0NzQyZDc0ODk0MTQ0M2MzMjhhNzVmNTExZWIzN2IwZDhiNjE2NDEzMCIsInNoYTI1Njo1NDBjZTk3MzFhNTI4NjdhMjlkYjY3YmM5YjdlYWQxMWRiMDQ5MThmMDBhNmU0MzI2MjdkMjFlMTE2MGVkOTJiIiwic2hhMjU2OmExNTUzNjNhNjdlZGY2ZTY0NjJhYTQ3NDQ1YzNkOTAwYjg0Y2VjMzMyMGI2MjQwNWI0OTVkZGY2NTAzNWMwMzkiXX19", "repoDigests": [ "anchore/test_images@sha256:4f6203a146e4c056f09fd72c687adfb23e75b18b58e3ea7c9a25a8af6699a381" ], "scope": "Squashed" } }, "distro": { "name": "alpine", "version": "3.13.4", "idLike": "" }, "descriptor": { "name": "syft", "version": "0.23.0" }, "schema": { "version": "1.1.0", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } ================================================ FILE: grype/pkg/testdata/syft-multiple-ecosystems.json ================================================ { "artifacts": [ { "id": "8039c8621bcc1383", "name": "alpine-baselayout", "version": "3.2.0-r6", "type": "apk", "foundBy": "apkdb-cataloger", "locations": [ { "path": "/lib/apk/db/installed", "layerID": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" } ], "licenses": [ "GPL-2.0-only" ], "language": "", "cpes": [ "cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r6:*:*:*:*:*:*:*" ], "purl": "pkg:alpine/alpine-baselayout@3.2.0-r6?arch=x86_64", "metadataType": "ApkMetadata", "metadata": { "package": "alpine-baselayout", "originPackage": "alpine-baselayout", "maintainer": "Natanael Copa ", "version": "3.2.0-r6", "license": "GPL-2.0-only", "architecture": "x86_64", "url": "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", "description": "Alpine base dir structure and init scripts", "size": 21101, "installedSize": 413696, "pullDependencies": "/bin/sh so:libc.musl-x86_64.so.1", "pullChecksum": "Q1EymS6rAgmGs7XYhqdyEoiWgEZ6A=", "gitCommitOfApkPort": "dfa1379357a321e638feef1cd8d55ab03d020f45", "files": [ { "path": "/dev" }, { "path": "/dev/pts" }, { "path": "/dev/shm" }, { "path": "/etc" }, { "path": "/etc/fstab", "digest": { "algorithm": "sha1", "value": "Q11Q7hNe8QpDS531guqCdrXBzoA/o=" } }, { "path": "/etc/group", "digest": { "algorithm": "sha1", "value": "Q13K+olJg5ayzHSVNUkggZJXuB+9Y=" } }, { "path": "/etc/hostname", "digest": { "algorithm": "sha1", "value": "Q16nVwYVXP/tChvUPdukVD2ifXOmc=" } }, { "path": "/etc/hosts", "digest": { "algorithm": "sha1", "value": "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=" } }, { "path": "/etc/inittab", "digest": { "algorithm": "sha1", "value": "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=" } }, { "path": "/etc/modules", "digest": { "algorithm": "sha1", "value": "Q1toogjUipHGcMgECgPJX64SwUT1M=" } }, { "path": "/etc/motd", "digest": { "algorithm": "sha1", "value": "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=" } }, { "path": "/etc/mtab", "ownerUid": "0", "ownerGid": "0", "permissions": "777", "digest": { "algorithm": "sha1", "value": "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=" } }, { "path": "/etc/passwd", "digest": { "algorithm": "sha1", "value": "Q1TchuuLUfur0izvfZQZxgN/LJhB8=" } }, { "path": "/etc/profile", "digest": { "algorithm": "sha1", "value": "Q1VmHPWPjjvz4oCsbmYCUB4uWpSkc=" } }, { "path": "/etc/protocols", "digest": { "algorithm": "sha1", "value": "Q1omKlp3vgGq2ZqYzyD/KHNdo8rDc=" } }, { "path": "/etc/services", "digest": { "algorithm": "sha1", "value": "Q19WLCv5ItKg4MH7RWfNRh1I7byQc=" } }, { "path": "/etc/shadow", "ownerUid": "0", "ownerGid": "42", "permissions": "640", "digest": { "algorithm": "sha1", "value": "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=" } }, { "path": "/etc/shells", "digest": { "algorithm": "sha1", "value": "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=" } }, { "path": "/etc/sysctl.conf", "digest": { "algorithm": "sha1", "value": "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=" } }, { "path": "/etc/apk" }, { "path": "/etc/conf.d" }, { "path": "/etc/crontabs" }, { "path": "/etc/crontabs/root", "ownerUid": "0", "ownerGid": "0", "permissions": "600", "digest": { "algorithm": "sha1", "value": "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=" } }, { "path": "/etc/init.d" }, { "path": "/etc/modprobe.d" }, { "path": "/etc/modprobe.d/aliases.conf", "digest": { "algorithm": "sha1", "value": "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=" } }, { "path": "/etc/modprobe.d/blacklist.conf", "digest": { "algorithm": "sha1", "value": "Q14TdgFHkTdt3uQC+NBtrntOnm9n4=" } }, { "path": "/etc/modprobe.d/i386.conf", "digest": { "algorithm": "sha1", "value": "Q1pnay/njn6ol9cCssL7KiZZ8etlc=" } }, { "path": "/etc/modprobe.d/kms.conf", "digest": { "algorithm": "sha1", "value": "Q1ynbLn3GYDpvajba/ldp1niayeog=" } }, { "path": "/etc/modules-load.d" }, { "path": "/etc/network" }, { "path": "/etc/network/if-down.d" }, { "path": "/etc/network/if-post-down.d" }, { "path": "/etc/network/if-pre-up.d" }, { "path": "/etc/network/if-up.d" }, { "path": "/etc/opt" }, { "path": "/etc/periodic" }, { "path": "/etc/periodic/15min" }, { "path": "/etc/periodic/daily" }, { "path": "/etc/periodic/hourly" }, { "path": "/etc/periodic/monthly" }, { "path": "/etc/periodic/weekly" }, { "path": "/etc/profile.d" }, { "path": "/etc/profile.d/README", "digest": { "algorithm": "sha1", "value": "Q135OWsCzzvnB2fmFx62kbqm1Ax1k=" } }, { "path": "/etc/profile.d/color_prompt.sh.disabled", "digest": { "algorithm": "sha1", "value": "Q10wL23GuSCVfumMRgakabUI6EsSk=" } }, { "path": "/etc/profile.d/locale.sh", "digest": { "algorithm": "sha1", "value": "Q1S8j+WW71mWxfVy8ythqU7HUVoBw=" } }, { "path": "/etc/sysctl.d" }, { "path": "/home" }, { "path": "/lib" }, { "path": "/lib/firmware" }, { "path": "/lib/mdev" }, { "path": "/lib/modules-load.d" }, { "path": "/lib/sysctl.d" }, { "path": "/lib/sysctl.d/00-alpine.conf", "digest": { "algorithm": "sha1", "value": "Q1HpElzW1xEgmKfERtTy7oommnq6c=" } }, { "path": "/media" }, { "path": "/media/cdrom" }, { "path": "/media/floppy" }, { "path": "/media/usb" }, { "path": "/mnt" }, { "path": "/opt" }, { "path": "/proc" }, { "path": "/root", "ownerUid": "0", "ownerGid": "0", "permissions": "700" }, { "path": "/run" }, { "path": "/sbin" }, { "path": "/sbin/mkmntdirs", "ownerUid": "0", "ownerGid": "0", "permissions": "755", "digest": { "algorithm": "sha1", "value": "Q1qjkdyRJcYblGC6RMqUR4Bdb5g10=" } }, { "path": "/srv" }, { "path": "/sys" }, { "path": "/tmp", "ownerUid": "0", "ownerGid": "0", "permissions": "1777" }, { "path": "/usr" }, { "path": "/usr/lib" }, { "path": "/usr/lib/modules-load.d" }, { "path": "/usr/local" }, { "path": "/usr/local/bin" }, { "path": "/usr/local/lib" }, { "path": "/usr/local/share" }, { "path": "/usr/sbin" }, { "path": "/usr/share" }, { "path": "/usr/share/man" }, { "path": "/usr/share/misc" }, { "path": "/var" }, { "path": "/var/run", "ownerUid": "0", "ownerGid": "0", "permissions": "777", "digest": { "algorithm": "sha1", "value": "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=" } }, { "path": "/var/cache" }, { "path": "/var/cache/misc" }, { "path": "/var/empty", "ownerUid": "0", "ownerGid": "0", "permissions": "555" }, { "path": "/var/lib" }, { "path": "/var/lib/misc" }, { "path": "/var/local" }, { "path": "/var/lock" }, { "path": "/var/lock/subsys" }, { "path": "/var/log" }, { "path": "/var/mail" }, { "path": "/var/opt" }, { "path": "/var/spool" }, { "path": "/var/spool/mail", "ownerUid": "0", "ownerGid": "0", "permissions": "777", "digest": { "algorithm": "sha1", "value": "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=" } }, { "path": "/var/spool/cron" }, { "path": "/var/spool/cron/crontabs", "ownerUid": "0", "ownerGid": "0", "permissions": "777", "digest": { "algorithm": "sha1", "value": "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=" } }, { "path": "/var/tmp", "ownerUid": "0", "ownerGid": "0", "permissions": "1777" } ] } }, { "name": "fake", "version": "1.2.0", "type": "dpkg", "foundBy": "dpkg-cataloger", "locations": [ { "path": "/lib/apk/db/installed", "layerID": "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c" } ], "licenses": [ "LGPL-3.0-or-later" ], "language": "lang", "cpes": [ "cpe:2.3:a:*:fake:1.2.0:*:*:*:*:*:*:*", "cpe:2.3:a:fake:fake:1.2.0:*:*:*:*:*:*:*" ], "purl": "pkg:deb/debian/fake@1.2.0?arch=x86_64", "metadataType": "DpkgMetadata", "metadata": { "source": "a-source", "sourceVersion": "1.4.5" } }, { "name": "gmp", "version": "6.2.0-r0", "type": "java-archive", "foundBy": "java-cataloger", "locations": [ { "path": "/lib/apk/db/installed", "layerID": "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c" } ], "licenses": [ "LGPL-3.0-or-later" ], "language": "the-lang", "cpes": [ "cpe:2.3:a:*:gmp:6.2.0-r0:*:*:*:*:*:*:*", "cpe:2.3:a:gmp:gmp:6.2.0-r0:*:*:*:*:*:*:*" ], "purl": "pkg:alpine/gmp@6.2.0-r0?arch=x86_64", "metadataType": "JavaMetadata", "metadata": { "pomProperties": { "groupId": "gid", "artifactId": "aid" }, "manifest": { "main": { "Name": "a-name" } } } } ], "source": { "type": "image", "target": { "userInput": "alpine:fake", "imageID": "sha256:fadf1294c09213b20d4d6fc84109584e1c102d185c2cae15144a87d29de65c6d", "manifestDigest": "sha256:1f6495428fb363e2d233e5df078b2b200635c4e51f0a3be34ecf09d44b547590", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ "alpine:fake" ], "imageSize": 15879684, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a", "size": 5570176 } ], "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMTE2LCJkaWdlc3QiOiJzaGEyNTY6ZmFkZjEyOTRjMDkyMTNiMjBkNGQ2ZmM4NDEwOTU4NGUxYzEwMmQxODVjMmNhZTE1MTQ0YTg3ZDI5ZGU2NWM2ZCJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODQ0OTkyLCJkaWdlc3QiOiJzaGEyNTY6NTA2NDRjMjllZjVhMjdjOWE0MGMzOTNhNzNlY2UyNDc5ZGU3ODMyNWNhZTdkNzYyZWYzY2RjMTliZjQyZGQwYSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE2NzkzNiwiZGlnZXN0Ijoic2hhMjU2OmNjMGZmMWRkYWQ2ZmU0OTc4ZDgzMjYzMGE5MzAzODgzYWRjNTZlZGZjNzdjYWEzNjkyMjM5YzJkODFjZjVkMDAifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMDE2Njc4NCwiZGlnZXN0Ijoic2hhMjU2OjNkZDJkYjQ4M2JjOWQ2YjU2MWNlNWNjMTEwNWUwYjZkMTk2MWNhMjQ5YTczNmJiYTgzNzFhYjI4ZWEzMDRmODQifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMjUyOCwiZGlnZXN0Ijoic2hhMjU2OjkzY2Y0Y2ZiNjczYzdlMTZhOWU3NGY3MzFkNjc2N2I3MGI5MmEwYjdjOWY1OWQwNmVmZDcyZmJmZjUzNTM3MWMifV19", "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiQXJnc0VzY2FwZWQiOnRydWUsIkltYWdlIjoic2hhMjU2OjJjOWQ1MzNiMmI2NGFiMTI4MmFlYTE2ZGYwZjlkYmYwYjNjZDQ3YWMxZTAyYjc1YTM3NjNiMmY0M2NjOWRlNWUiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY29udGFpbmVyIjoiYzJlMTM3OTEyYWU2MzdkNzBlMDJhMDVhYWEyM2U3N2JlY2I3Mzg5MDJmZDNjYWMyMjdkNDRlYjdlYzEwMmQ0OCIsImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IiIsIkF0dGFjaFN0ZGluIjpmYWxzZSwiQXR0YWNoU3Rkb3V0IjpmYWxzZSwiQXR0YWNoU3RkZXJyIjpmYWxzZSwiVHR5IjpmYWxzZSwiT3BlblN0ZGluIjpmYWxzZSwiU3RkaW5PbmNlIjpmYWxzZSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIl0sIkNtZCI6WyIvYmluL3NoIiwiLWMiLCJzZWQgLWkgJ3MvVjowLjkuMTEtcjMvVjowLjkuOS1yMC8nIC9saWIvYXBrL2RiL2luc3RhbGxlZCJdLCJJbWFnZSI6InNoYTI1NjoyYzlkNTMzYjJiNjRhYjEyODJhZWExNmRmMGY5ZGJmMGIzY2Q0N2FjMWUwMmI3NWEzNzYzYjJmNDNjYzlkZTVlIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNyZWF0ZWQiOiIyMDIwLTA5LTI0VDIyOjI2OjQ2LjE2NzYxOTRaIiwiZG9ja2VyX3ZlcnNpb24iOiIxOS4wMy4xMiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIwLTA1LTI5VDIxOjE5OjQ2LjE5MjA0NTk3MloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YzkyYzI0ODIzOWY4YzdiOWIzYzA2NzY1MDk1NDgxNWYzOTFiN2JjYjA5MDIzZjk4NDk3MmMwODJhY2UyYThkMCBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjAtMDUtMjlUMjE6MTk6NDYuMzYzNTE4MzQ1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMC0wOS0yNFQyMjoyNjo0NC4zMjk1NTc4WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jIHdnZXQgaHR0cDovL2RsLWNkbi5hbHBpbmVsaW51eC5vcmcvYWxwaW5lL3YzLjkvbWFpbi94ODZfNjQvbGlidm5jc2VydmVyLTAuOS4xMS1yMy5hcGsifSx7ImNyZWF0ZWQiOiIyMDIwLTA5LTI0VDIyOjI2OjQ1LjY3MDg1MzhaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgYXBrIGFkZCAgbGlidm5jc2VydmVyLTAuOS4xMS1yMy5hcGsifSx7ImNyZWF0ZWQiOiIyMDIwLTA5LTI0VDIyOjI2OjQ2LjE2NzYxOTRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgc2VkIC1pICdzL1Y6MC45LjExLXIzL1Y6MC45LjktcjAvJyAvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1Njo1MDY0NGMyOWVmNWEyN2M5YTQwYzM5M2E3M2VjZTI0NzlkZTc4MzI1Y2FlN2Q3NjJlZjNjZGMxOWJmNDJkZDBhIiwic2hhMjU2OmNjMGZmMWRkYWQ2ZmU0OTc4ZDgzMjYzMGE5MzAzODgzYWRjNTZlZGZjNzdjYWEzNjkyMjM5YzJkODFjZjVkMDAiLCJzaGEyNTY6M2RkMmRiNDgzYmM5ZDZiNTYxY2U1Y2MxMTA1ZTBiNmQxOTYxY2EyNDlhNzM2YmJhODM3MWFiMjhlYTMwNGY4NCIsInNoYTI1Njo5M2NmNGNmYjY3M2M3ZTE2YTllNzRmNzMxZDY3NjdiNzBiOTJhMGI3YzlmNTlkMDZlZmQ3MmZiZmY1MzUzNzFjIl19fQ==" } }, "distro": { "name": "alpine", "version": "3.12.0", "idLike": "" }, "descriptor": { "name": "syft", "version": "[not provided]" }, "schema": { "version": "1.0.0", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.0.json" } } ================================================ FILE: grype/pkg/testdata/syft-spring.json ================================================ { "artifacts": [ { "name": "charsets", "version": "", "type": "java-archive", "foundBy": "java-cataloger", "locations": [ { "path": "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar", "layerID": "sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393" } ], "licenses": [], "language": "java", "cpes": [ "cpe:2.3:a:charsets:charsets:*:*:*:*:*:java:*:*", "cpe:2.3:a:charsets:charsets:*:*:*:*:*:maven:*:*" ], "purl": "", "metadataType": "JavaMetadata", "metadata": { "virtualPath": "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar", "manifest": { "main": { "Created-By": "1.8.0_242 (Oracle Corporation)", "Manifest-Version": "1.0" } } } }, { "name": "tomcat-embed-el", "version": "9.0.27", "type": "java-archive", "foundBy": "java-cataloger", "locations": [ { "path": "/app/libs/tomcat-embed-el-9.0.27.jar", "layerID": "sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4" } ], "licenses": [], "language": "java", "cpes": [ "cpe:2.3:a:tomcat_embed_el:tomcat-embed-el:9.0.27:*:*:*:*:java:*:*", "cpe:2.3:a:tomcat-embed-el:tomcat_embed_el:9.0.27:*:*:*:*:maven:*:*" ], "purl": "", "metadataType": "JavaMetadata", "metadata": { "virtualPath": "/app/libs/tomcat-embed-el-9.0.27.jar", "manifest": { "main": { "Ant-Version": "Apache Ant 1.9.9", "Automatic-Module-Name": "org.apache.tomcat.embed.jasper.el", "Bnd-LastModified": "1570442272460", "Bundle-ManifestVersion": "2", "Bundle-Name": "tomcat-embed-jasper-el", "Bundle-SymbolicName": "org.apache.tomcat-embed-jasper-el", "Bundle-Version": "9.0.27", "Created-By": "1.8.0_222 (AdoptOpenJDK)", "DSTAMP": "20191007", "Export-Package": "javax.el,org.apache.el;uses:=\"javax.el,org.apache.el.parser\",org.apache.el.lang;uses:=\"javax.el,org.apache.el.parser\",org.apache.el.parser;uses:=\"javax.el,org.apache.el.lang\"", "Implementation-Title": "Apache Tomcat", "Implementation-Vendor": "Apache Software Foundation", "Implementation-Version": "9.0.27", "Import-Package": "javax.el,javax.servlet.jsp.el", "Manifest-Version": "1.0", "Originally-Created-By": "1.8.0_222-b10 ()", "Private-Package": "org.apache.el.stream,org.apache.el.util", "Require-Capability": "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"", "Specification-Title": "Apache Tomcat", "Specification-Vendor": "Apache Software Foundation", "Specification-Version": "9.0", "TODAY": "October 7 2019", "TSTAMP": "1057", "Tool": "Bnd-4.2.0.201903051501", "X-Compile-Source-JDK": "8", "X-Compile-Target-JDK": "8" } } } } ], "artifactRelationships": [], "source": { "type": "image", "target": { "userInput": "springio/gs-spring-boot-docker:latest", "imageID": "sha256:9065659c6e537b0364b7b1d3e5442a3a5aa56d755fb883d221e9e8b3637fb58e", "manifestDigest": "sha256:be3d8a5f700d4c45f3ed324b95d9f028f587c135bc85cf87e193414db521d533", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ "springio/gs-spring-boot-docker:latest" ], "imageSize": 142807921, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602", "size": 1809479 } ], "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNTk1LCJkaWdlc3QiOiJzaGEyNTY6OTA2NTY1OWM2ZTUzN2IwMzY0YjdiMWQzZTU0NDJhM2E1YWE1NmQ3NTVmYjg4M2QyMjFlOWU4YjM2MzdmYjU4ZSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjozMDYxNzYwLCJkaWdlc3QiOiJzaGEyNTY6NDJhMzAyN2VhYWMxNTBkMmI4ZjUxNjEwMDkyMWY0YmQ4M2IzZGJjMjBiZmU2NDEyNGY2ODZjMDcyYjQ5YzYwMiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE1NDQxOTIwLCJkaWdlc3QiOiJzaGEyNTY6ZjQ3MTYzZThkZTU3ZTNlM2NjZmU4OWQ1ZGZiZDljMjUyZDllY2E1M2RjNzkwNmI4ZGI2MGVkZGNiODc2YzU5MiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE5NjYwODAsImRpZ2VzdCI6InNoYTI1Njo2MTg5YWJlMDk1ZDUzYzFjOWYyYmZjOGY1MDEyOGVlODc2YjlhNWQxMGY5ZWRhMTU2NGU1ZjUzNTdkNmZmZTYxIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MTA2ODMzOTIwLCJkaWdlc3QiOiJzaGEyNTY6YTFhNmNlYWRiNzAxYWI0ZTZjOTNiMjQzZGMyYTBkYWVkYzhjZWUyM2EyNDIwMzg0NWVjY2NkNTc4NGNkMTM5MyJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE3NDg3ODcyLCJkaWdlc3QiOiJzaGEyNTY6ODk1MDRmMDgzZDNmMTUzMjJmOTdhZTI0MGRmNDQ2NTAyMDNmMjQ0Mjc4NjBkYjFiM2QzMmU2NmRkMDU5NDBlNCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1NjoyNDQzNDk3MWNhN2Y0MGUxYTdlNjRlZThlYTFjYTg0MzQzMmRhMWUxYmIxYzU5ODM4NzA4MzUwNjU2ODBkMTU0In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2OjNjMDEwMjY1NDQ5ZDAwZmFlZTZmMmZhMWM3ZGY0ODEwOWMwZDcwOGM3MWJlZTRhMzhlNGI1ZTBmYTliODdjZTkifV19", "config": "eyJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgiLCJjb25maWciOnsiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIiwiU1NMX0NFUlRfRklMRT0vZXRjL3NzbC9jZXJ0cy9jYS1jZXJ0aWZpY2F0ZXMuY3J0IiwiSkFWQV9WRVJTSU9OPTh1MjQyIl0sIkVudHJ5cG9pbnQiOlsiamF2YSIsIi1jcCIsIi9hcHAvcmVzb3VyY2VzOi9hcHAvY2xhc3NlczovYXBwL2xpYnMvKiIsImhlbGxvLkFwcGxpY2F0aW9uIl0sIkV4cG9zZWRQb3J0cyI6e30sIkxhYmVscyI6e30sIlZvbHVtZXMiOnt9fSwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjE5NzAtMDEtMDFUMDA6MDA6MDBaIiwiYXV0aG9yIjoiQmF6ZWwiLCJjcmVhdGVkX2J5IjoiYmF6ZWwgYnVpbGQgLi4uIn0seyJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJhdXRob3IiOiJCYXplbCIsImNyZWF0ZWRfYnkiOiJiYXplbCBidWlsZCAuLi4ifSx7ImNyZWF0ZWQiOiIxOTcwLTAxLTAxVDAwOjAwOjAwWiIsImF1dGhvciI6IkJhemVsIiwiY3JlYXRlZF9ieSI6ImJhemVsIGJ1aWxkIC4uLiJ9LHsiY3JlYXRlZCI6IjE5NzAtMDEtMDFUMDA6MDA6MDBaIiwiYXV0aG9yIjoiQmF6ZWwiLCJjcmVhdGVkX2J5IjoiYmF6ZWwgYnVpbGQgLi4uIn0seyJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJhdXRob3IiOiJKaWIiLCJjcmVhdGVkX2J5IjoiamliLW1hdmVuLXBsdWdpbjoyLjIuMCIsImNvbW1lbnQiOiJkZXBlbmRlbmNpZXMifSx7ImNyZWF0ZWQiOiIxOTcwLTAxLTAxVDAwOjAwOjAwWiIsImF1dGhvciI6IkppYiIsImNyZWF0ZWRfYnkiOiJqaWItbWF2ZW4tcGx1Z2luOjIuMi4wIiwiY29tbWVudCI6InJlc291cmNlcyJ9LHsiY3JlYXRlZCI6IjE5NzAtMDEtMDFUMDA6MDA6MDBaIiwiYXV0aG9yIjoiSmliIiwiY3JlYXRlZF9ieSI6ImppYi1tYXZlbi1wbHVnaW46Mi4yLjAiLCJjb21tZW50IjoiY2xhc3NlcyJ9XSwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NDJhMzAyN2VhYWMxNTBkMmI4ZjUxNjEwMDkyMWY0YmQ4M2IzZGJjMjBiZmU2NDEyNGY2ODZjMDcyYjQ5YzYwMiIsInNoYTI1NjpmNDcxNjNlOGRlNTdlM2UzY2NmZTg5ZDVkZmJkOWMyNTJkOWVjYTUzZGM3OTA2YjhkYjYwZWRkY2I4NzZjNTkyIiwic2hhMjU2OjYxODlhYmUwOTVkNTNjMWM5ZjJiZmM4ZjUwMTI4ZWU4NzZiOWE1ZDEwZjllZGExNTY0ZTVmNTM1N2Q2ZmZlNjEiLCJzaGEyNTY6YTFhNmNlYWRiNzAxYWI0ZTZjOTNiMjQzZGMyYTBkYWVkYzhjZWUyM2EyNDIwMzg0NWVjY2NkNTc4NGNkMTM5MyIsInNoYTI1Njo4OTUwNGYwODNkM2YxNTMyMmY5N2FlMjQwZGY0NDY1MDIwM2YyNDQyNzg2MGRiMWIzZDMyZTY2ZGQwNTk0MGU0Iiwic2hhMjU2OjI0NDM0OTcxY2E3ZjQwZTFhN2U2NGVlOGVhMWNhODQzNDMyZGExZTFiYjFjNTk4Mzg3MDgzNTA2NTY4MGQxNTQiLCJzaGEyNTY6M2MwMTAyNjU0NDlkMDBmYWVlNmYyZmExYzdkZjQ4MTA5YzBkNzA4YzcxYmVlNGEzOGU0YjVlMGZhOWI4N2NlOSJdfX0=", "repoDigests": [ "springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08" ], "scope": "Squashed" } }, "distro": { "name": "debian", "version": "9", "idLike": "" }, "descriptor": { "name": "syft", "version": "[not provided]" }, "schema": { "version": "1.1.0", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } ================================================ FILE: grype/pkg/upstream_package.go ================================================ package pkg import ( "strings" "github.com/scylladb/go-set/strset" "github.com/anchore/syft/syft/cpe" ) type UpstreamPackage struct { Name string // the package name Version string // the version of the package } func UpstreamPackages(p Package) (pkgs []Package) { original := p for _, u := range p.Upstreams { tmp := original if u.Name == "" { continue } tmp.Name = u.Name if u.Version != "" { tmp.Version = u.Version } tmp.Upstreams = nil // for each cpe, replace pkg name with origin and add to set cpeStrings := strset.New() for _, c := range tmp.CPEs { if u.Version != "" { c.Attributes.Version = u.Version } // use BindToFmtString because we search against unescaped CPE strings updatedCPEString := strings.ReplaceAll(c.Attributes.BindToFmtString(), p.Name, u.Name) cpeStrings.Add(updatedCPEString) } // with each entry in set, convert string to CPE and update the new CPEs var updatedCPEs []cpe.CPE for _, cpeString := range cpeStrings.List() { updatedCPE, _ := cpe.New(cpeString, "") updatedCPEs = append(updatedCPEs, updatedCPE) } tmp.CPEs = updatedCPEs pkgs = append(pkgs, tmp) } return pkgs } ================================================ FILE: grype/pkg/upstream_package_test.go ================================================ package pkg import ( "testing" "github.com/stretchr/testify/assert" "github.com/anchore/syft/syft/cpe" ) func TestUpstreamPackages(t *testing.T) { tests := []struct { name string pkg Package expected []Package }{ { name: "no upstreams results in empty list", pkg: Package{ Name: "name", Version: "version", }, expected: nil, }, { name: "with upstream name", pkg: Package{ Name: "name", Version: "version", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*", ""), }, Upstreams: []UpstreamPackage{ { Name: "new-name", }, }, }, expected: []Package{ { Name: "new-name", // new Version: "version", // original CPEs: []cpe.CPE{ // name and vendor replaced cpe.Must("cpe:2.3:*:new-name:new-name:version:*:*:*:*:*:*:*", ""), }, // no upstreams }, }, }, { name: "with upstream name and version", pkg: Package{ Name: "name", Version: "version", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*", ""), }, Upstreams: []UpstreamPackage{ { Name: "new-name", Version: "new-version", }, }, }, expected: []Package{ { Name: "new-name", // new Version: "new-version", // new CPEs: []cpe.CPE{ // name, vendor, and version replaced cpe.Must("cpe:2.3:*:new-name:new-name:new-version:*:*:*:*:*:*:*", ""), }, // no upstreams }, }, }, { name: "no upstream name results in no package", pkg: Package{ Name: "name", Version: "version", CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:name:name:version:*:*:*:*:*:*:*", ""), }, Upstreams: []UpstreamPackage{ { // note: invalid without a name Version: "new-version", }, }, }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var actual []Package actual = append(actual, UpstreamPackages(tt.pkg)...) assert.Equalf(t, tt.expected, actual, "UpstreamPackages(%v)", tt.pkg) }) } } ================================================ FILE: grype/pkg/version_format.go ================================================ package pkg import ( "github.com/anchore/grype/grype/version" syftPkg "github.com/anchore/syft/syft/pkg" ) func VersionFormat(p Package) version.Format { switch p.Type { case syftPkg.ApkPkg: return version.ApkFormat case syftPkg.BitnamiPkg: return version.BitnamiFormat case syftPkg.DebPkg: return version.DebFormat case syftPkg.JavaPkg: return version.MavenFormat case syftPkg.RpmPkg: return version.RpmFormat case syftPkg.GemPkg: return version.GemFormat case syftPkg.PythonPkg: return version.PythonFormat case syftPkg.KbPkg: return version.KBFormat case syftPkg.PortagePkg: return version.PortageFormat case syftPkg.GoModulePkg: return version.GolangFormat case syftPkg.AlpmPkg: return version.PacmanFormat } if isJvmPackage(p) { return version.JVMFormat } return version.UnknownFormat } ================================================ FILE: grype/pkg/version_format_test.go ================================================ package pkg import ( "fmt" "testing" "github.com/anchore/grype/grype/version" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestVersionFormat(t *testing.T) { tests := []struct { name string p Package format version.Format }{ { name: "bitnami", p: Package{ Type: syftPkg.BitnamiPkg, }, format: version.BitnamiFormat, }, { name: "deb", p: Package{ Type: syftPkg.DebPkg, }, format: version.DebFormat, }, { name: "java jar", p: Package{ Type: syftPkg.JavaPkg, }, format: version.MavenFormat, }, { name: "gem", p: Package{ Type: syftPkg.GemPkg, }, format: version.GemFormat, }, { name: "alpm (arch linux)", p: Package{ Type: syftPkg.AlpmPkg, }, format: version.PacmanFormat, }, { name: "jvm by metadata", p: Package{ Metadata: JavaVMInstallationMetadata{}, }, format: version.JVMFormat, }, { name: "jvm by type and name (jdk)", p: Package{ Type: syftPkg.BinaryPkg, Name: "jdk", }, format: version.JVMFormat, }, { name: "jvm by type and name (openjdk)", p: Package{ Type: syftPkg.BinaryPkg, Name: "openjdk", }, format: version.JVMFormat, }, { name: "jvm by type and name (jre)", p: Package{ Type: syftPkg.BinaryPkg, Name: "jre", }, format: version.JVMFormat, }, { name: "jvm by type and name (java_se)", p: Package{ Type: syftPkg.BinaryPkg, Name: "java_se", }, format: version.JVMFormat, }, } for _, test := range tests { name := fmt.Sprintf("pkgType[%s]->format[%s]", test.p.Type, test.format) t.Run(name, func(t *testing.T) { actual := VersionFormat(test.p) if actual != test.format { t.Errorf("mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'", test.p.Type, test.format, actual) } }) } } ================================================ FILE: grype/presenter/cyclonedx/presenter.go ================================================ package cyclonedx import ( "io" "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/clio" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/syft/syft/format/common/cyclonedxhelpers" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) // Presenter writes a CycloneDX report from the given Matches and Scope contents type Presenter struct { id clio.Identification document models.Document src source.Description format cyclonedx.BOMFileFormat sbom *sbom.SBOM } // NewJSONPresenter is a *Presenter constructor func NewJSONPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ id: pb.ID, document: pb.Document, src: pb.SBOM.Source, sbom: pb.SBOM, format: cyclonedx.BOMFileFormatJSON, } } // NewXMLPresenter is a *Presenter constructor func NewXMLPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ id: pb.ID, document: pb.Document, src: pb.SBOM.Source, sbom: pb.SBOM, format: cyclonedx.BOMFileFormatXML, } } // Present creates a CycloneDX-based reporting func (p *Presenter) Present(output io.Writer) error { // note: this uses the syft cyclondx helpers to create // a consistent cyclondx BOM across syft and grype cyclonedxBOM := cyclonedxhelpers.ToFormatModel(*p.sbom) // empty the tool metadata and add grype metadata cyclonedxBOM.Metadata.Tools = &cyclonedx.ToolsChoice{ Components: &[]cyclonedx.Component{ { Type: cyclonedx.ComponentTypeApplication, Author: "anchore", Name: p.id.Name, Version: p.id.Version, }, }, } vulns := make([]cyclonedx.Vulnerability, 0) for _, m := range p.document.Matches { v, err := NewVulnerability(m) if err != nil { continue } vulns = append(vulns, v) } cyclonedxBOM.Vulnerabilities = &vulns enc := cyclonedx.NewBOMEncoder(output, p.format) enc.SetPretty(true) enc.SetEscapeHTML(false) return enc.EncodeVersion(cyclonedxBOM, cyclonedxBOM.SpecVersion) } ================================================ FILE: grype/presenter/cyclonedx/presenter_test.go ================================================ package cyclonedx import ( "bytes" "flag" "fmt" "os/exec" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal/testutils" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/sbom" ) var update = flag.Bool("update", false, "update the *.golden files for cyclonedx presenters") var validatorImage = "cyclonedx/cyclonedx-cli:0.27.2@sha256:829c9ea8f2104698bc3c1228575bfa495f6cc4ec151329323c013ca94408477f" func Test_CycloneDX_Valid(t *testing.T) { if _, err := exec.LookPath("docker"); err != nil { t.Skip("docker not available") } tests := []struct { name string scheme internal.SyftSource }{ { name: "json directory", scheme: internal.DirectorySource, }, { name: "json image", scheme: internal.ImageSource, }, { name: "xml directory", scheme: internal.DirectorySource, }, { name: "xml image", scheme: internal.ImageSource, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() format := strings.Split(tc.name, " ")[0] var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, tc.scheme) var pres *Presenter switch format { case "json": pres = NewJSONPresenter(pb) case "xml": pres = NewXMLPresenter(pb) default: t.Fatalf("invalid format: %s", format) } err := pres.Present(&buffer) require.NoError(t, err) contents := buffer.String() cmd := exec.Command("docker", "run", "--rm", "-i", "--entrypoint", "/bin/sh", validatorImage, "-c", fmt.Sprintf("tee &> /dev/null && cyclonedx validate --input-version v1_6 --fail-on-errors --input-format %s", format)) out := bytes.Buffer{} cmd.Stdout = &out cmd.Stderr = &out // pipe to the docker command cmd.Stdin = strings.NewReader(contents) err = cmd.Run() if err != nil || cmd.ProcessState.ExitCode() != 0 { // not valid t.Fatalf("error validating CycloneDX %s document: %s \nBOM:\n%s", format, out.String(), contents) } }) } } func Test_noTypedNils(t *testing.T) { s := sbom.SBOM{ Artifacts: sbom.Artifacts{ FileMetadata: map[file.Coordinates]file.Metadata{}, FileDigests: map[file.Coordinates][]file.Digest{}, }, } c := file.NewCoordinates("/file", "123") s.Artifacts.FileMetadata[c] = file.Metadata{ Path: "/file", } s.Artifacts.FileDigests[c] = []file.Digest{} p := NewJSONPresenter(models.PresenterConfig{ SBOM: &s, Pretty: false, }) contents := bytes.Buffer{} err := p.Present(&contents) require.NoError(t, err) require.NotContains(t, contents.String(), "null") } func TestCycloneDxPresenterImage(t *testing.T) { var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, internal.ImageSource) pres := NewJSONPresenter(pb) // run presenter err := pres.Present(&buffer) if err != nil { t.Fatal(err) } actual := buffer.Bytes() if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which are tested independently actual = internal.Redact(actual) expected = internal.Redact(expected) if d := cmp.Diff(string(expected), string(actual)); d != "" { t.Fatalf("diff: %s", d) } } func TestCycloneDxPresenterDir(t *testing.T) { var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, internal.DirectorySource) pres := NewJSONPresenter(pb) // run presenter err := pres.Present(&buffer) if err != nil { t.Fatal(err) } actual := buffer.Bytes() if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which are tested independently actual = internal.Redact(actual) expected = internal.Redact(expected) if d := cmp.Diff(string(expected), string(actual)); d != "" { t.Fatalf("diff: %s", d) } } ================================================ FILE: grype/presenter/cyclonedx/testdata/snapshot/TestCycloneDxPresenterDir.golden ================================================ { "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.6", "serialNumber": "urn:uuid:802d9c0b-716e-4050-a832-9a697dc0d796", "version": 1, "metadata": { "timestamp": "2026-03-19T11:34:26-04:00", "tools": { "components": [ { "type": "application", "author": "anchore", "name": "grype", "version": "[not provided]" } ] }, "component": { "bom-ref": "163686ac6e30c752", "type": "file", "name": "/var/folders/09/zjmdnk0n4496cmzrdkxbw1tr0000gn/T/TestCycloneDxPresenterDir688002288/001" } }, "components": [ { "bom-ref": "bbb0ba712c2b94ea", "type": "library", "name": "package-1", "version": "1.1.1", "cpe": "cpe:2.3:a:anchore\\:oss:anchore\\/engine:0.9.2:*:*:en:*:*:*:*", "properties": [ { "name": "syft:package:type", "value": "rpm" }, { "name": "syft:package:metadataType", "value": "rpm-db-entry" }, { "name": "syft:location:0:path", "value": "/foo/bar/somefile-1.txt" }, { "name": "syft:metadata:epoch", "value": "2" }, { "name": "syft:metadata:size", "value": "0" }, { "name": "syft:metadata:sourceRpm", "value": "some-source-rpm" } ] }, { "bom-ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625", "type": "library", "name": "package-2", "version": "2.2.2", "licenses": [ { "license": { "id": "Apache-2.0" } }, { "license": { "id": "MIT" } } ], "cpe": "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*", "purl": "pkg:deb/package-2@2.2.2", "properties": [ { "name": "syft:package:type", "value": "deb" }, { "name": "syft:location:0:path", "value": "/foo/bar/somefile-2.txt" } ] } ], "vulnerabilities": [ { "bom-ref": "urn:uuid:a73ee074-a643-466a-82ba-1669a7fa5c1d", "id": "CVE-1999-0001", "source": {}, "references": [ { "id": "CVE-1999-0001", "source": {} } ], "ratings": [ { "score": 8.2, "severity": "low", "method": "CVSSv31", "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H" }, { "source": { "name": "FIRST", "url": "https://www.first.org/epss/" }, "score": 0.03, "method": "other" } ], "affects": [ { "ref": "bbb0ba712c2b94ea" } ] }, { "bom-ref": "urn:uuid:7324150f-e4ab-4d68-8dd8-b9721b4a0eed", "id": "CVE-1999-0002", "source": {}, "references": [ { "id": "CVE-1999-0002", "source": {} } ], "ratings": [ { "score": 8.5, "severity": "critical", "method": "CVSSv31", "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H" }, { "source": { "name": "FIRST", "url": "https://www.first.org/epss/" }, "score": 0.08, "method": "other" }, { "source": { "name": "CISA KEV Catalog", "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog" }, "score": 1, "method": "other", "justification": "Listed in CISA KEV" } ], "affects": [ { "ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625" } ] } ] } ================================================ FILE: grype/presenter/cyclonedx/testdata/snapshot/TestCycloneDxPresenterImage.golden ================================================ { "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.6", "serialNumber": "urn:uuid:491fc8dc-9384-4f3d-a729-d2c5a83791d8", "version": 1, "metadata": { "timestamp": "2026-03-19T11:34:26-04:00", "tools": { "components": [ { "type": "application", "author": "anchore", "name": "grype", "version": "[not provided]" } ] }, "component": { "bom-ref": "1882f79f937f7d91", "type": "container", "name": "user-input", "version": "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5" } }, "components": [ { "bom-ref": "bbb0ba712c2b94ea", "type": "library", "name": "package-1", "version": "1.1.1", "cpe": "cpe:2.3:a:anchore\\:oss:anchore\\/engine:0.9.2:*:*:en:*:*:*:*", "properties": [ { "name": "syft:package:type", "value": "rpm" }, { "name": "syft:package:metadataType", "value": "rpm-db-entry" }, { "name": "syft:location:0:path", "value": "/foo/bar/somefile-1.txt" }, { "name": "syft:metadata:epoch", "value": "2" }, { "name": "syft:metadata:size", "value": "0" }, { "name": "syft:metadata:sourceRpm", "value": "some-source-rpm" } ] }, { "bom-ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625", "type": "library", "name": "package-2", "version": "2.2.2", "licenses": [ { "license": { "id": "Apache-2.0" } }, { "license": { "id": "MIT" } } ], "cpe": "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*", "purl": "pkg:deb/package-2@2.2.2", "properties": [ { "name": "syft:package:type", "value": "deb" }, { "name": "syft:location:0:path", "value": "/foo/bar/somefile-2.txt" } ] } ], "vulnerabilities": [ { "bom-ref": "urn:uuid:8f52aaa6-907a-43e7-8b48-836453a51bbf", "id": "CVE-1999-0001", "source": {}, "references": [ { "id": "CVE-1999-0001", "source": {} } ], "ratings": [ { "score": 8.2, "severity": "low", "method": "CVSSv31", "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H" }, { "source": { "name": "FIRST", "url": "https://www.first.org/epss/" }, "score": 0.03, "method": "other" } ], "affects": [ { "ref": "bbb0ba712c2b94ea" } ] }, { "bom-ref": "urn:uuid:166daf49-c343-460e-b66a-c22247147678", "id": "CVE-1999-0002", "source": {}, "references": [ { "id": "CVE-1999-0002", "source": {} } ], "ratings": [ { "score": 8.5, "severity": "critical", "method": "CVSSv31", "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H" }, { "source": { "name": "FIRST", "url": "https://www.first.org/epss/" }, "score": 0.08, "method": "other" }, { "source": { "name": "CISA KEV Catalog", "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog" }, "score": 1, "method": "other", "justification": "Listed in CISA KEV" } ], "affects": [ { "ref": "pkg:deb/package-2@2.2.2?package-id=74378afe15713625" } ] } ] } ================================================ FILE: grype/presenter/cyclonedx/vulnerability.go ================================================ package cyclonedx import ( "strconv" "strings" "github.com/CycloneDX/cyclonedx-go" "github.com/google/uuid" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/packageurl-go" ) // https://cyclonedx.org/docs/1.4/json/#vulnerabilities_items_bom-ref // NewVulnerability creates a Vulnerability document from a match and the metadata provider func NewVulnerability(m models.Match) (v cyclonedx.Vulnerability, err error) { metadata := m.Vulnerability.VulnerabilityMetadata ratings := generateCDXRatings(metadata) source := &cyclonedx.Source{ Name: cdxSourceName(metadata.Namespace), URL: metadata.DataSource, } references := &[]cyclonedx.VulnerabilityReference{ { ID: m.Vulnerability.ID, Source: source, }, } advisories := &[]cyclonedx.Advisory{} for _, advisory := range metadata.URLs { *advisories = append(*advisories, cyclonedx.Advisory{ URL: advisory, }) } // Note: if a field isn't captured here it's usually because the resulting // reference link contains that information for the consumer return cyclonedx.Vulnerability{ BOMRef: uuid.New().URN(), ID: m.Vulnerability.ID, Source: source, References: references, Ratings: &ratings, // We do not capture CWEs in our model CWEs: nil, Description: metadata.Description, // We do not capture the full detailed description in our model Detail: "", // We do not capture the recommendations in our model Recommendation: "", Advisories: advisories, Affects: &[]cyclonedx.Affects{ { Ref: deriveBomRef(m.Artifact), }, }, // Data source creation Created: "", // Vulnerability first published Published: "", // Vulnerability last updated Updated: "", // We do not capture acredited in our model Credits: nil, // We do not capture information about the method used to determine the vulnerability pre publishing Tools: nil, // TODO: we do not leverage the following fields in our model Analysis: nil, Properties: nil, }, nil } func generateCDXRatings(metadata models.VulnerabilityMetadata) []cyclonedx.VulnerabilityRating { severity := cdxSeverityFromGrypeSeverity(metadata.Severity) ratings := make([]cyclonedx.VulnerabilityRating, 0) for _, cvss := range metadata.Cvss { var rating cyclonedx.VulnerabilityRating score := cvss.Metrics.BaseScore rating.Score = &score // Scoring method can be one of the following: // "CVSSv2", "CVSSv3", "CVSSv31", "OWASP", "other" method, err := cvssVersionToMethod(cvss.Version) if err != nil { // do not halt execution if one CVSS fails to provide an accurate Version // TODO: log warning here? continue } rating.Method = method rating.Vector = cvss.Vector rating.Severity = severity ratings = append(ratings, rating) } // ensure the severity is always included if len(ratings) == 0 { ratings = append(ratings, cyclonedx.VulnerabilityRating{ Severity: severity, }) } // Add EPSS score if available if len(metadata.EPSS) > 0 { epssScore := metadata.EPSS[0].EPSS ratings = append(ratings, cyclonedx.VulnerabilityRating{ Method: cyclonedx.ScoringMethod("EPSS"), Score: &epssScore, Source: &cyclonedx.Source{ Name: "FIRST", URL: "https://www.first.org/epss/", }, }) } // Add KEV indication if available if len(metadata.KnownExploited) > 0 { kevScore := 1.0 ratings = append(ratings, cyclonedx.VulnerabilityRating{ Method: cyclonedx.ScoringMethodOther, Score: &kevScore, Source: &cyclonedx.Source{ Name: "CISA KEV Catalog", URL: "https://www.cisa.gov/known-exploited-vulnerabilities-catalog", }, Justification: "Listed in CISA KEV", }) } return ratings } // cvssVersionToMethod accepts a CVSS version as string (e.g. "3.1") and converts it to a // CycloneDx rating Method, for example "CVSSv3" func cvssVersionToMethod(version string) (cyclonedx.ScoringMethod, error) { value, err := strconv.ParseFloat(version, 64) if err != nil { return "", err } switch value { case 2: return cyclonedx.ScoringMethodCVSSv2, nil case 3: return cyclonedx.ScoringMethodCVSSv3, nil case 3.1: return cyclonedx.ScoringMethodCVSSv31, nil default: return cyclonedx.ScoringMethodOther, nil } } // takes namespace: eg debian:distro:debian:10 // returns source name: eg debian-distrot-debian-10 func cdxSourceName(namespace string) string { return strings.ReplaceAll(namespace, ":", "-") } func cdxSeverityFromGrypeSeverity(severity string) cyclonedx.Severity { switch severity { case "Negligible": return cyclonedx.SeverityNone case "Unknown": return cyclonedx.SeverityUnknown case "Info": return cyclonedx.SeverityInfo case "Low": return cyclonedx.SeverityLow case "Medium": return cyclonedx.SeverityMedium case "High": return cyclonedx.SeverityHigh case "Critical": return cyclonedx.SeverityCritical default: return cyclonedx.SeverityUnknown } } func deriveBomRef(p models.Package) string { // try and parse the PURL if possible and append syft id to it, to make // the purl unique in the BOM. // TODO: In the future we may want to dedupe by PURL and combine components with // the same PURL while preserving their unique metadata. if parsedPURL, err := packageurl.FromString(p.PURL); err == nil { parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: p.ID}) return parsedPURL.ToString() } // fallback is to use strictly the ID if there is no valid pURL return p.ID } ================================================ FILE: grype/presenter/cyclonedx/vulnerability_test.go ================================================ package cyclonedx import ( "testing" "github.com/CycloneDX/cyclonedx-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" ) func TestCvssVersionToMethod(t *testing.T) { testCases := []struct { desc string input string expected cyclonedx.ScoringMethod errors bool }{ { desc: "invalid (not float)", input: "", expected: "", errors: true, }, { desc: "CVSS v2", input: "2.0", expected: cyclonedx.ScoringMethodCVSSv2, errors: false, }, { desc: "CVSS v31", input: "3.1", expected: cyclonedx.ScoringMethodCVSSv31, errors: false, }, { desc: "CVSS v3", input: "3", expected: cyclonedx.ScoringMethodCVSSv3, errors: false, }, { desc: "invalid (no match)", input: "15.4", expected: cyclonedx.ScoringMethodOther, errors: false, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { actual, err := cvssVersionToMethod(tc.input) if !tc.errors { assert.NoError(t, err) } else { assert.Error(t, err) } assert.Equal(t, tc.expected, actual) }) } } type metadataProvider struct { severity string cvss []vulnerability.Cvss } func (m metadataProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { return &vulnerability.Metadata{ ID: ref.ID, DataSource: "", Namespace: ref.Namespace, Severity: m.severity, URLs: nil, Description: "", Cvss: m.cvss, }, nil } func TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) { tests := []struct { name string match models.Match }{ { name: "populates severity with missing CVSS records", match: models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: models.VulnerabilityMetadata{ Severity: "High", }, }, Artifact: models.Package{}, MatchDetails: nil, }, }, { name: "populates severity with all CVSS records", match: models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: models.VulnerabilityMetadata{ Severity: "High", Cvss: []models.Cvss{ { Version: "2.0", Metrics: models.CvssMetrics{ BaseScore: 1.1, }, }, { Version: "3.0", Metrics: models.CvssMetrics{ BaseScore: 2.1, }, }, { Version: "3.1", Metrics: models.CvssMetrics{ BaseScore: 3.1, }, }, }, }, }, Artifact: models.Package{}, MatchDetails: nil, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := NewVulnerability(test.match) require.NoError(t, err) require.NotNil(t, actual.Ratings, "cyclonedx document ratings should not be nil") require.NotEmpty(t, actual.Ratings) require.Equal(t, cdxSeverityFromGrypeSeverity(test.match.Vulnerability.Severity), (*actual.Ratings)[0].Severity) if len(test.match.Vulnerability.Cvss) > 0 { for i, rating := range *actual.Ratings { require.Equal(t, test.match.Vulnerability.Cvss[i].Metrics.BaseScore, *rating.Score) } } }) } } func TestNewVulnerability_IncludesEPSSAndKEV(t *testing.T) { match := models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: "CVE-2025-0001", Severity: "High", EPSS: []models.EPSS{ { EPSS: 0.87, }, }, KnownExploited: []models.KnownExploited{ { KnownRansomwareCampaignUse: "known", }, }, }, }, Artifact: models.Package{}, MatchDetails: nil, } vuln, err := NewVulnerability(match) require.NoError(t, err) ratings := *vuln.Ratings require.Len(t, ratings, 3, "should include 1 CVSS + 1 EPSS + 1 KEV rating") var foundEPSS, foundKEV bool for _, r := range ratings { if r.Method == "EPSS" { foundEPSS = true assert.NotNil(t, r.Score) assert.InDelta(t, 0.87, *r.Score, 0.001) assert.Equal(t, "FIRST", r.Source.Name) } if r.Method == "other" && r.Source != nil && r.Source.Name == "CISA KEV Catalog" { foundKEV = true assert.NotNil(t, r.Score) assert.Equal(t, 1.0, *r.Score) } } assert.True(t, foundEPSS, "should include EPSS rating") assert.True(t, foundKEV, "should include KEV rating") } ================================================ FILE: grype/presenter/explain/__snapshots__/explain_snapshot_test.snap ================================================ [TestExplainSnapshot/keycloak-CVE-2020-12413 - 1] CVE-2020-12413 from nvd:cpe (Medium) The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites. Related vulnerabilities: - redhat:distro:redhat:9 CVE-2020-12413 (Low) Matched packages: - Package: nss, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Match explanation(s): - redhat:distro:redhat:9:CVE-2020-12413 Direct match (package name, version, and ecosystem) against nss (version 3.79.0-17.el9_1). Locations: - /var/lib/rpm/rpmdb.sqlite - Package: nspr, version: 4.34.0-17.el9_1 PURL: pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Match explanation(s): - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. Locations: - /var/lib/rpm/rpmdb.sqlite - Package: nss-softokn, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Match explanation(s): - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. Locations: - /var/lib/rpm/rpmdb.sqlite - Package: nss-softokn-freebl, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Match explanation(s): - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. Locations: - /var/lib/rpm/rpmdb.sqlite - Package: nss-sysinit, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Match explanation(s): - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. Locations: - /var/lib/rpm/rpmdb.sqlite - Package: nss-util, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Match explanation(s): - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. Locations: - /var/lib/rpm/rpmdb.sqlite URLs: - https://nvd.nist.gov/vuln/detail/CVE-2020-12413 - https://access.redhat.com/security/cve/CVE-2020-12413 --- [TestExplainSnapshot/chainguard-ruby-CVE-2023-28755 - 1] CVE-2023-28755 from nvd:cpe (High) A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1. Related vulnerabilities: - github:language:ruby GHSA-hv5j-3h9f-99c2 (High) - wolfi:distro:wolfi:rolling CVE-2023-28755 (High) Matched packages: - Package: ruby-3.0, version: 3.0.4-r1 PURL: pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118 Match explanation(s): - wolfi:distro:wolfi:rolling:CVE-2023-28755 Direct match (package name, version, and ecosystem) against ruby-3.0 (version 3.0.4-r1). - nvd:cpe:CVE-2023-28755 CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*`. - wolfi:distro:wolfi:rolling:CVE-2023-28755 Indirect match; this CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package. Locations: - /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec - /lib/apk/db/installed URLs: - https://nvd.nist.gov/vuln/detail/CVE-2023-28755 - https://github.com/advisories/GHSA-hv5j-3h9f-99c2 - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755 --- [TestExplainSnapshot/test_a_GHSA - 1] GHSA-cfh5-3ghh-wfjx from github:language:java (Medium) Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient Related vulnerabilities: - nvd:cpe CVE-2014-3577 (Medium) Matched packages: - Package: httpclient, version: 4.1.1 PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 Match explanation(s): - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1). Locations: - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient URLs: - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 --- [TestExplainSnapshot/test_a_CVE_alias_of_a_GHSA - 1] CVE-2014-3577 from nvd:cpe (Medium) org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field. Related vulnerabilities: - github:language:java GHSA-cfh5-3ghh-wfjx (Medium) Matched packages: - Package: httpclient, version: 4.1.1 PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 Match explanation(s): - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1). - nvd:cpe:CVE-2014-3577 CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*`. Locations: - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient URLs: - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx --- ================================================ FILE: grype/presenter/explain/explain.go ================================================ package explain import ( _ "embed" "fmt" "io" "sort" "strings" "text/template" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/syft/syft/file" ) //go:embed explain_cve.tmpl var explainTemplate string type VulnerabilityExplainer interface { ExplainByID(IDs []string) error ExplainBySeverity(severity string) error ExplainAll() error } type ViewModel struct { PrimaryVulnerability models.VulnerabilityMetadata RelatedVulnerabilities []models.VulnerabilityMetadata MatchedPackages []*explainedPackage // I think this needs a map of artifacts to explained evidence URLs []string } type viewModelBuilder struct { PrimaryMatch models.Match // The match that seems to be the one we're trying to explain RelatedMatches []models.Match requestedIDs []string // the vulnerability IDs the user requested explanations of } type Findings map[string]ViewModel type explainedPackage struct { PURL string Name string Version string MatchedOnID string MatchedOnNamespace string IndirectExplanation string DirectExplanation string CPEExplanation string Locations []explainedEvidence displayPriority int // shows how early it should be displayed; direct matches first } type explainedEvidence struct { Location string ArtifactID string ViaVulnID string ViaNamespace string } type vulnerabilityExplainer struct { w io.Writer doc *models.Document } func NewVulnerabilityExplainer(w io.Writer, doc *models.Document) VulnerabilityExplainer { return &vulnerabilityExplainer{ w: w, doc: doc, } } var funcs = template.FuncMap{ "trim": strings.TrimSpace, } func (e *vulnerabilityExplainer) ExplainByID(ids []string) error { findings, err := Doc(e.doc, ids) if err != nil { return err } t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate)) for _, id := range ids { finding, ok := findings[id] if !ok { continue } if err := t.Execute(e.w, finding); err != nil { return fmt.Errorf("unable to execute template: %w", err) } } return nil } func (e *vulnerabilityExplainer) ExplainBySeverity(_ string) error { return fmt.Errorf("not implemented") } func (e *vulnerabilityExplainer) ExplainAll() error { findings, err := Doc(e.doc, nil) if err != nil { return err } t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate)) return t.Execute(e.w, findings) } func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { result := make(Findings) builders := make(map[string]*viewModelBuilder) for _, m := range doc.Matches { key := m.Vulnerability.ID existing, ok := builders[key] if !ok { existing = newBuilder(requestedIDs) builders[m.Vulnerability.ID] = existing } existing.WithMatch(m, requestedIDs) } for _, m := range doc.Matches { for _, related := range m.RelatedVulnerabilities { key := related.ID existing, ok := builders[key] if !ok { existing = newBuilder(requestedIDs) builders[key] = existing } existing.WithMatch(m, requestedIDs) } } for k, v := range builders { result[k] = v.Build() } return result, nil } func newBuilder(requestedIDs []string) *viewModelBuilder { return &viewModelBuilder{ requestedIDs: requestedIDs, } } // WithMatch adds a match to the builder // accepting enough information to determine whether the match is a primary match or a related match func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) { if b.isPrimaryAdd(m, userRequestedIDs) { // Demote the current primary match to related match // if it exists if b.PrimaryMatch.Vulnerability.ID != "" { b.WithRelatedMatch(b.PrimaryMatch) } b.WithPrimaryMatch(m) } else { b.WithRelatedMatch(m) } } func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool { if b.PrimaryMatch.Vulnerability.ID == "" { return true } idWasRequested := false for _, id := range userRequestedIDs { if candidate.Vulnerability.ID == id { idWasRequested = true break } } // the user didn't ask about this ID, so it's not the primary one if !idWasRequested && len(userRequestedIDs) > 0 { return false } // NVD CPEs are somewhat canonical IDs for vulnerabilities, so if the user asked about CVE-YYYY-ID // type number, and we have a record from NVD, consider that the primary record. if candidate.Vulnerability.Namespace == "nvd:cpe" { return true } // Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for. for _, related := range b.PrimaryMatch.RelatedVulnerabilities { if related.ID == candidate.Vulnerability.ID { return true } } return false } func (b *viewModelBuilder) WithPrimaryMatch(m models.Match) *viewModelBuilder { b.PrimaryMatch = m return b } func (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder { b.RelatedMatches = append(b.RelatedMatches, m) return b } func (b *viewModelBuilder) Build() ViewModel { explainedPackages := groupAndSortEvidence(append(b.RelatedMatches, b.PrimaryMatch)) var relatedVulnerabilities []models.VulnerabilityMetadata dedupeRelatedVulnerabilities := make(map[string]models.VulnerabilityMetadata) var sortDedupedRelatedVulnerabilities []string for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID) dedupeRelatedVulnerabilities[key] = m.Vulnerability.VulnerabilityMetadata for _, r := range m.RelatedVulnerabilities { key := fmt.Sprintf("%s:%s", r.Namespace, r.ID) dedupeRelatedVulnerabilities[key] = r } } // delete the primary vulnerability from the related vulnerabilities so it isn't listed twice primary := b.primaryVulnerability() delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primary.Namespace, primary.ID)) for k := range dedupeRelatedVulnerabilities { sortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k) } sort.Strings(sortDedupedRelatedVulnerabilities) for _, k := range sortDedupedRelatedVulnerabilities { relatedVulnerabilities = append(relatedVulnerabilities, dedupeRelatedVulnerabilities[k]) } return ViewModel{ PrimaryVulnerability: primary, RelatedVulnerabilities: relatedVulnerabilities, MatchedPackages: explainedPackages, URLs: b.dedupeAndSortURLs(primary), } } func (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata { var primaryVulnerability models.VulnerabilityMetadata for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { for _, r := range append(m.RelatedVulnerabilities, m.Vulnerability.VulnerabilityMetadata) { if r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == "nvd:cpe" { primaryVulnerability = r } } } if primaryVulnerability.ID == "" { primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata } return primaryVulnerability } // nolint:funlen func groupAndSortEvidence(matches []models.Match) []*explainedPackage { idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range matches { key := m.Artifact.ID var newLocations []explainedEvidence for _, l := range m.Artifact.Locations { newLocations = append(newLocations, explainLocation(m, l)) } var directExplanation string var indirectExplanation string var cpeExplanation string var matchTypePriority int for i, md := range m.MatchDetails { explanation := explainMatchDetail(m, i) if explanation != "" { switch md.Type { case string(match.CPEMatch): cpeExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) matchTypePriority = 1 // cpes are a type of direct match case string(match.ExactIndirectMatch): indirectExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) matchTypePriority = 0 // display indirect matches after direct matches case string(match.ExactDirectMatch): directExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) matchTypePriority = 2 // exact-direct-matches are high confidence, direct matches; display them first. } } } e, ok := idsToMatchDetails[key] if !ok { e = &explainedPackage{ PURL: m.Artifact.PURL, Name: m.Artifact.Name, Version: m.Artifact.Version, MatchedOnID: m.Vulnerability.ID, MatchedOnNamespace: m.Vulnerability.Namespace, DirectExplanation: directExplanation, IndirectExplanation: indirectExplanation, CPEExplanation: cpeExplanation, Locations: newLocations, displayPriority: matchTypePriority, } idsToMatchDetails[key] = e } else { e.Locations = append(e.Locations, newLocations...) if e.CPEExplanation == "" { e.CPEExplanation = cpeExplanation } if e.IndirectExplanation == "" { e.IndirectExplanation = indirectExplanation } e.displayPriority += matchTypePriority } } var sortIDs []string for k, v := range idsToMatchDetails { sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]explainedEvidence) for _, l := range v.Locations { dedupeLocations[l.Location] = l } var uniqueLocations []explainedEvidence for _, l := range dedupeLocations { uniqueLocations = append(uniqueLocations, l) } sort.Slice(uniqueLocations, func(i, j int) bool { if uniqueLocations[i].ViaNamespace == uniqueLocations[j].ViaNamespace { return uniqueLocations[i].Location < uniqueLocations[j].Location } return uniqueLocations[i].ViaNamespace < uniqueLocations[j].ViaNamespace }) v.Locations = uniqueLocations } sort.Slice(sortIDs, func(i, j int) bool { return explainedPackageIsLess(idsToMatchDetails[sortIDs[i]], idsToMatchDetails[sortIDs[j]]) }) var explainedPackages []*explainedPackage for _, k := range sortIDs { explainedPackages = append(explainedPackages, idsToMatchDetails[k]) } return explainedPackages } func explainedPackageIsLess(i, j *explainedPackage) bool { if i.displayPriority != j.displayPriority { return i.displayPriority > j.displayPriority } return i.Name < j.Name } func explainMatchDetail(m models.Match, index int) string { if len(m.MatchDetails) <= index { return "" } md := m.MatchDetails[index] explanation := "" switch md.Type { case string(match.CPEMatch): explanation = formatCPEExplanation(m) case string(match.ExactIndirectMatch): sourceName, sourceVersion := sourcePackageNameAndVersion(md) explanation = fmt.Sprintf("Indirect match; this CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) case string(match.ExactDirectMatch): explanation = fmt.Sprintf("Direct match (package name, version, and ecosystem) against %s (version %s).", m.Artifact.Name, m.Artifact.Version) } return explanation } // dedupeAndSortURLs returns a slice of the DataSource fields, deduplicated and sorted // the NVD and GHSA URL are given special treatment; they return first and second if present // and the rest are sorted by string sort. func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { showFirst := primaryVulnerability.DataSource var URLs []string URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { URLs = append(URLs, v.DataSource) } for _, m := range b.RelatedMatches { URLs = append(URLs, m.Vulnerability.DataSource) for _, v := range m.RelatedVulnerabilities { URLs = append(URLs, v.DataSource) } } var result []string deduplicate := make(map[string]bool) result = append(result, showFirst) deduplicate[showFirst] = true nvdURL := "" ghsaURL := "" for _, u := range URLs { if strings.HasPrefix(u, "https://nvd.nist.gov/vuln/detail") { nvdURL = u } if strings.HasPrefix(u, "https://github.com/advisories") { ghsaURL = u } } if nvdURL != "" && nvdURL != showFirst { result = append(result, nvdURL) deduplicate[nvdURL] = true } if ghsaURL != "" && ghsaURL != showFirst { result = append(result, ghsaURL) deduplicate[ghsaURL] = true } for _, u := range URLs { if _, ok := deduplicate[u]; !ok { result = append(result, u) deduplicate[u] = true } } return result } func explainLocation(match models.Match, location file.Location) explainedEvidence { path := location.RealPath if javaMeta, ok := match.Artifact.Metadata.(map[string]any); ok { if virtPath, ok := javaMeta["virtualPath"].(string); ok { path = virtPath } } return explainedEvidence{ Location: path, ArtifactID: match.Artifact.ID, ViaVulnID: match.Vulnerability.ID, ViaNamespace: match.Vulnerability.Namespace, } } func formatCPEExplanation(m models.Match) string { searchedBy := m.MatchDetails[0].SearchedBy if mapResult, ok := searchedBy.(map[string]interface{}); ok { if cpes, ok := mapResult["cpes"]; ok { if cpeSlice, ok := cpes.([]interface{}); ok { if len(cpeSlice) > 0 { return fmt.Sprintf("CPE match on `%s`.", cpeSlice[0]) } } } } return "" } func sourcePackageNameAndVersion(md models.MatchDetails) (string, string) { var name string var version string if mapResult, ok := md.SearchedBy.(map[string]interface{}); ok { if sourcePackage, ok := mapResult["package"]; ok { if sourceMap, ok := sourcePackage.(map[string]interface{}); ok { if maybeName, ok := sourceMap["name"]; ok { name, _ = maybeName.(string) } if maybeVersion, ok := sourceMap["version"]; ok { version, _ = maybeVersion.(string) } } } } return name, version } func nameForUpstream(typ string) string { switch typ { case "deb": return "origin" case "rpm": return "source RPM" } return "upstream" } ================================================ FILE: grype/presenter/explain/explain_cve.tmpl ================================================ {{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }}) {{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} Matched packages:{{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} PURL: {{ .PURL }}{{ end }} Match explanation(s):{{ if .DirectExplanation }} - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }} - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} - {{ .IndirectExplanation }}{{ end }} Locations:{{ range .Locations }} - {{ .Location }}{{ end }}{{ end }} URLs:{{ range .URLs }} - {{ . }}{{ end }} ================================================ FILE: grype/presenter/explain/explain_snapshot_test.go ================================================ package explain_test import ( "bytes" "encoding/json" "os" "testing" "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/presenter/explain" "github.com/anchore/grype/grype/presenter/models" ) func TestExplainSnapshot(t *testing.T) { // load sample json testCases := []struct { name string fixture string vulnerabilityIDs []string }{ { name: "keycloak-CVE-2020-12413", fixture: "./testdata/keycloak-test.json", vulnerabilityIDs: []string{"CVE-2020-12413"}, }, { name: "chainguard-ruby-CVE-2023-28755", fixture: "testdata/chainguard-ruby-test.json", vulnerabilityIDs: []string{"CVE-2023-28755"}, }, { name: "test a GHSA", /* fixture created by: Saving output of grype anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da -o json Then filtering matches to relevant ones: jq -c '.matches[]' | rg -e GHSA-cfh5-3ghh-wfjx -e CVE-2014-3577 | jq -s . */ fixture: "testdata/ghsa-test.json", vulnerabilityIDs: []string{"GHSA-cfh5-3ghh-wfjx"}, }, { name: "test a CVE alias of a GHSA", fixture: "testdata/ghsa-test.json", vulnerabilityIDs: []string{"CVE-2014-3577"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r, err := os.Open(tc.fixture) require.NoError(t, err) // parse to models.Document doc := models.Document{} decoder := json.NewDecoder(r) err = decoder.Decode(&doc) require.NoError(t, err) // create explain.VulnerabilityExplainer w := bytes.NewBufferString("") explainer := explain.NewVulnerabilityExplainer(w, &doc) // call ExplainByID err = explainer.ExplainByID(tc.vulnerabilityIDs) require.NoError(t, err) // assert output snaps.MatchSnapshot(t, w.String()) }) } } ================================================ FILE: grype/presenter/explain/testdata/chainguard-ruby-test.json ================================================ { "matches": [ { "vulnerability": { "id": "CVE-2023-28755", "dataSource": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755", "namespace": "wolfi:distro:wolfi:rolling", "severity": "High", "urls": [ "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755" ], "cvss": [], "fix": { "versions": [ "3.0.6-r0" ], "state": "fixed" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2023-28755", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755", "namespace": "nvd:cpe", "severity": "High", "urls": [ "https://github.com/ruby/uri/releases/", "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/", "https://www.ruby-lang.org/en/downloads/releases/", "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/", "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/" ], "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "metrics": { "baseScore": 7.5, "exploitabilityScore": 3.9, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "apk-matcher", "searchedBy": { "distro": { "type": "wolfi", "version": "20221118" }, "namespace": "wolfi:distro:wolfi:rolling", "package": { "name": "ruby-3.0", "version": "3.0.4-r1" } }, "found": { "versionConstraint": "< 3.0.6-r0 (apk)", "vulnerabilityID": "CVE-2023-28755" } }, { "type": "exact-direct-match", "matcher": "apk-matcher", "searchedBy": { "distro": { "type": "wolfi", "version": "20221118" }, "namespace": "wolfi:distro:wolfi:rolling", "package": { "name": "ruby-3.0", "version": "3.0.4-r1" } }, "found": { "versionConstraint": "< 3.0.6-r0 (apk)", "vulnerabilityID": "CVE-2023-28755" } } ], "artifact": { "name": "ruby-3.0", "version": "3.0.4-r1", "type": "apk", "locations": [ { "path": "/lib/apk/db/installed", "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880" } ], "language": "", "licenses": [ "PSF-2.0" ], "cpes": [ "cpe:2.3:a:ruby-lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-lang:ruby:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_lang:ruby:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby:ruby:3.0.4-r1:*:*:*:*:*:*:*" ], "purl": "pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118", "upstreams": [ { "name": "ruby-3.0" } ] } }, { "vulnerability": { "id": "CVE-2023-28755", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755", "namespace": "nvd:cpe", "severity": "High", "urls": [ "https://github.com/ruby/uri/releases/", "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/", "https://www.ruby-lang.org/en/downloads/releases/", "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/", "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/" ], "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "metrics": { "baseScore": 7.5, "exploitabilityScore": 3.9, "impactScore": 3.6 }, "vendorMetadata": {} } ], "fix": { "versions": [], "state": "unknown" }, "advisories": [] }, "relatedVulnerabilities": [], "matchDetails": [ { "type": "cpe-match", "matcher": "ruby-gem-matcher", "searchedBy": { "namespace": "nvd:cpe", "cpes": [ "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*" ] }, "found": { "vulnerabilityID": "CVE-2023-28755", "versionConstraint": "<= 0.10.0 || = 0.10.1 || = 0.11.0 || = 0.12.0 (unknown)", "cpes": [ "cpe:2.3:a:ruby-lang:uri:*:*:*:*:*:ruby:*:*", "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:ruby:*:*" ] } } ], "artifact": { "name": "uri", "version": "0.10.1", "type": "gem", "locations": [ { "path": "/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec", "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880" } ], "language": "ruby", "licenses": [ "Ruby", "BSD-2-Clause" ], "cpes": [ "cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*" ], "purl": "pkg:gem/uri@0.10.1", "upstreams": [] } }, { "vulnerability": { "id": "GHSA-hv5j-3h9f-99c2", "dataSource": "https://github.com/advisories/GHSA-hv5j-3h9f-99c2", "namespace": "github:language:ruby", "severity": "High", "urls": [ "https://github.com/advisories/GHSA-hv5j-3h9f-99c2" ], "description": "Ruby URI component ReDoS issue", "cvss": [], "fix": { "versions": [ "0.10.2" ], "state": "fixed" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2023-28755", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755", "namespace": "nvd:cpe", "severity": "High", "urls": [ "https://github.com/ruby/uri/releases/", "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/", "https://www.ruby-lang.org/en/downloads/releases/", "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/", "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/" ], "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "metrics": { "baseScore": 7.5, "exploitabilityScore": 3.9, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-direct-match", "matcher": "ruby-gem-matcher", "searchedBy": { "language": "ruby", "namespace": "github:language:ruby" }, "found": { "versionConstraint": "=0.10.1 (unknown)", "vulnerabilityID": "GHSA-hv5j-3h9f-99c2" } } ], "artifact": { "name": "uri", "version": "0.10.1", "type": "gem", "locations": [ { "path": "/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec", "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880" } ], "language": "ruby", "licenses": [ "Ruby", "BSD-2-Clause" ], "cpes": [ "cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*", "cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*" ], "purl": "pkg:gem/uri@0.10.1", "upstreams": [] } } ], "source": { "type": "image", "target": { "userInput": "cgr.dev/chainguard/ruby:latest-3.0", "imageID": "sha256:2f88265cfbc43ca35cd327347a9f59375b9f29ef998b8a54a882e31111266640", "manifestDigest": "sha256:86abe662dfa3746038eea6b0db91092b0767d78b8b8938a343d614cd1579adc2", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ "cgr.dev/chainguard/ruby:latest-3.0" ], "imageSize": 38865264, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880", "size": 38865264 } ], "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo1NTMsImRpZ2VzdCI6InNoYTI1NjoyZjg4MjY1Y2ZiYzQzY2EzNWNkMzI3MzQ3YTlmNTkzNzViOWYyOWVmOTk4YjhhNTRhODgyZTMxMTExMjY2NjQwIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQxMTYxNzI4LCJkaWdlc3QiOiJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJ9XX0=", "config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImF1dGhvciI6ImdpdGh1Yi5jb20vY2hhaW5ndWFyZC1kZXYvYXBrbyIsImNyZWF0ZWQiOiIyMDIzLTAxLTEzVDAwOjExOjI2WiIsImhpc3RvcnkiOlt7ImF1dGhvciI6ImFwa28iLCJjcmVhdGVkIjoiMjAyMy0wMS0xM1QwMDoxMToyNloiLCJjcmVhdGVkX2J5IjoiYXBrbyIsImNvbW1lbnQiOiJUaGlzIGlzIGFuIGFwa28gc2luZ2xlLWxheWVyIGltYWdlIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJdfSwiY29uZmlnIjp7IkNtZCI6WyIvdXNyL2Jpbi9pcmIiXSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIiwiU1NMX0NFUlRfRklMRT0vZXRjL3NzbC9jZXJ0cy9jYS1jZXJ0aWZpY2F0ZXMuY3J0Il0sIlVzZXIiOiI2NTUzMiIsIldvcmtpbmdEaXIiOiIvd29yayJ9fQ==", "repoDigests": [ "cgr.dev/chainguard/ruby@sha256:3c9afb4f188827ea1062ec3b8acea32893236a0d7df31e0498df93486cff0978" ], "architecture": "arm64", "os": "linux" } }, "distro": { "name": "wolfi", "version": "20221118", "idLike": [] }, "descriptor": { "name": "grype", "version": "0.61.1", "configuration": { "configPath": "", "verbosity": 0, "output": "json", "file": "", "distro": "", "add-cpes-if-none": false, "output-template-file": "", "check-for-app-update": true, "only-fixed": false, "only-notfixed": false, "platform": "", "search": { "scope": "Squashed", "unindexed-archives": false, "indexed-archives": true }, "ignore": null, "exclude": [], "db": { "cache-dir": "/Users/willmurphy/Library/Caches/grype/db", "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", "ca-cert": "", "auto-update": true, "validate-by-hash-on-start": false, "validate-age": true, "max-allowed-built-age": 432000000000000 }, "externalSources": { "enable": false, "maven": { "searchUpstreamBySha1": true, "baseUrl": "https://search.maven.org/solrsearch/select" } }, "match": { "java": { "using-cpes": true }, "dotnet": { "using-cpes": true }, "golang": { "using-cpes": true }, "javascript": { "using-cpes": false }, "python": { "using-cpes": true }, "ruby": { "using-cpes": true }, "stock": { "using-cpes": true } }, "dev": { "profile-cpu": false, "profile-mem": false }, "fail-on-severity": "", "registry": { "insecure-skip-tls-verify": false, "insecure-use-http": false, "auth": [] }, "log": { "quiet": false, "verbosity": 0, "level": "warn", "file": "" }, "show-suppressed": false, "by-cve": false, "name": "", "default-image-pull-source": "" }, "db": { "built": "2023-05-17T01:32:43Z", "schemaVersion": 5, "location": "/Users/willmurphy/Library/Caches/grype/db/5", "checksum": "sha256:84ebb8325f426565e7a0cd00b2ea265a0ee0ec69db158a65541a42fddd1e15b0", "error": null }, "timestamp": "2023-05-17T21:00:56.783213-04:00" } } ================================================ FILE: grype/presenter/explain/testdata/ghsa-test.json ================================================ { "matches": [ { "vulnerability": { "id": "GHSA-cfh5-3ghh-wfjx", "dataSource": "https://github.com/advisories/GHSA-cfh5-3ghh-wfjx", "namespace": "github:language:java", "severity": "Medium", "urls": [ "https://github.com/advisories/GHSA-cfh5-3ghh-wfjx" ], "description": "Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient", "cvss": [], "fix": { "versions": [ "4.3.5" ], "state": "fixed" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2014-3577", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2014-3577", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html", "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html", "http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html", "http://rhn.redhat.com/errata/RHSA-2014-1146.html", "http://rhn.redhat.com/errata/RHSA-2014-1166.html", "http://rhn.redhat.com/errata/RHSA-2014-1833.html", "http://rhn.redhat.com/errata/RHSA-2014-1834.html", "http://rhn.redhat.com/errata/RHSA-2014-1835.html", "http://rhn.redhat.com/errata/RHSA-2014-1836.html", "http://rhn.redhat.com/errata/RHSA-2014-1891.html", "http://rhn.redhat.com/errata/RHSA-2014-1892.html", "http://rhn.redhat.com/errata/RHSA-2015-0125.html", "http://rhn.redhat.com/errata/RHSA-2015-0158.html", "http://rhn.redhat.com/errata/RHSA-2015-0675.html", "http://rhn.redhat.com/errata/RHSA-2015-0720.html", "http://rhn.redhat.com/errata/RHSA-2015-0765.html", "http://rhn.redhat.com/errata/RHSA-2015-0850.html", "http://rhn.redhat.com/errata/RHSA-2015-0851.html", "http://rhn.redhat.com/errata/RHSA-2015-1176.html", "http://rhn.redhat.com/errata/RHSA-2015-1177.html", "http://rhn.redhat.com/errata/RHSA-2015-1888.html", "http://rhn.redhat.com/errata/RHSA-2016-1773.html", "http://rhn.redhat.com/errata/RHSA-2016-1931.html", "http://seclists.org/fulldisclosure/2014/Aug/48", "http://secunia.com/advisories/60466", "http://www.openwall.com/lists/oss-security/2021/10/06/1", "http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html", "http://www.osvdb.org/110143", "http://www.securityfocus.com/bid/69258", "http://www.securitytracker.com/id/1030812", "http://www.ubuntu.com/usn/USN-2769-1", "https://access.redhat.com/solutions/1165533", "https://exchange.xforce.ibmcloud.com/vulnerabilities/95327", "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564", "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782", "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", "https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E" ], "description": "org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "2.0", "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", "metrics": { "baseScore": 5.8, "exploitabilityScore": 8.6, "impactScore": 4.9 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-direct-match", "matcher": "java-matcher", "searchedBy": { "language": "java", "namespace": "github:language:java", "package": { "name": "httpclient", "version": "4.1.1" } }, "found": { "versionConstraint": "<4.3.5 (unknown)", "vulnerabilityID": "GHSA-cfh5-3ghh-wfjx" } } ], "artifact": { "id": "f09cdae46b001bc5", "name": "httpclient", "version": "4.1.1", "type": "java-archive", "locations": [ { "path": "/TwilioNotifier.hpi", "layerID": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9" } ], "language": "java", "licenses": [], "cpes": [ "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*" ], "purl": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1", "upstreams": [], "metadataType": "JavaMetadata", "metadata": { "virtualPath": "/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient", "pomArtifactID": "httpclient", "pomGroupID": "org.apache.httpcomponents", "manifestName": "", "archiveDigests": null } } }, { "vulnerability": { "id": "CVE-2014-3577", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2014-3577", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html", "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html", "http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html", "http://rhn.redhat.com/errata/RHSA-2014-1146.html", "http://rhn.redhat.com/errata/RHSA-2014-1166.html", "http://rhn.redhat.com/errata/RHSA-2014-1833.html", "http://rhn.redhat.com/errata/RHSA-2014-1834.html", "http://rhn.redhat.com/errata/RHSA-2014-1835.html", "http://rhn.redhat.com/errata/RHSA-2014-1836.html", "http://rhn.redhat.com/errata/RHSA-2014-1891.html", "http://rhn.redhat.com/errata/RHSA-2014-1892.html", "http://rhn.redhat.com/errata/RHSA-2015-0125.html", "http://rhn.redhat.com/errata/RHSA-2015-0158.html", "http://rhn.redhat.com/errata/RHSA-2015-0675.html", "http://rhn.redhat.com/errata/RHSA-2015-0720.html", "http://rhn.redhat.com/errata/RHSA-2015-0765.html", "http://rhn.redhat.com/errata/RHSA-2015-0850.html", "http://rhn.redhat.com/errata/RHSA-2015-0851.html", "http://rhn.redhat.com/errata/RHSA-2015-1176.html", "http://rhn.redhat.com/errata/RHSA-2015-1177.html", "http://rhn.redhat.com/errata/RHSA-2015-1888.html", "http://rhn.redhat.com/errata/RHSA-2016-1773.html", "http://rhn.redhat.com/errata/RHSA-2016-1931.html", "http://seclists.org/fulldisclosure/2014/Aug/48", "http://secunia.com/advisories/60466", "http://www.openwall.com/lists/oss-security/2021/10/06/1", "http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html", "http://www.osvdb.org/110143", "http://www.securityfocus.com/bid/69258", "http://www.securitytracker.com/id/1030812", "http://www.ubuntu.com/usn/USN-2769-1", "https://access.redhat.com/solutions/1165533", "https://exchange.xforce.ibmcloud.com/vulnerabilities/95327", "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564", "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782", "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", "https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E", "https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E" ], "description": "org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "2.0", "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", "metrics": { "baseScore": 5.8, "exploitabilityScore": 8.6, "impactScore": 4.9 }, "vendorMetadata": {} } ], "fix": { "versions": [], "state": "unknown" }, "advisories": [] }, "relatedVulnerabilities": [], "matchDetails": [ { "type": "cpe-match", "matcher": "java-matcher", "searchedBy": { "namespace": "nvd:cpe", "cpes": [ "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*" ], "Package": { "name": "httpclient", "version": "4.1.1" } }, "found": { "vulnerabilityID": "CVE-2014-3577", "versionConstraint": ">= 4.0, <= 4.3.4 (unknown)", "cpes": [ "cpe:2.3:a:apache:httpclient:*:*:*:*:*:*:*:*" ] } } ], "artifact": { "id": "f09cdae46b001bc5", "name": "httpclient", "version": "4.1.1", "type": "java-archive", "locations": [ { "path": "/TwilioNotifier.hpi", "layerID": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9" } ], "language": "java", "licenses": [], "cpes": [ "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*" ], "purl": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1", "upstreams": [], "metadataType": "JavaMetadata", "metadata": { "virtualPath": "/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient", "pomArtifactID": "httpclient", "pomGroupID": "org.apache.httpcomponents", "manifestName": "", "archiveDigests": null } } } ], "source": { "type": "image", "target": { "userInput": "anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da", "imageID": "sha256:e1a0913e5e6eb346f15791e9627842ae80b14564f9c7a4f2e0910a9433673d8b", "manifestDigest": "sha256:1212e7636ec0b1a7b90eb354e761e67163c2256de127036f086876e190631b43", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [], "imageSize": 42104079, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68", "size": 5590942 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9", "size": 36511427 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:5d5007f009bb615228db4046d5cae910563859d1e3a37cadb2d691ea783ad8a7", "size": 1710 } ], "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMTU5LCJkaWdlc3QiOiJzaGEyNTY6ZTFhMDkxM2U1ZTZlYjM0NmYxNTc5MWU5NjI3ODQyYWU4MGIxNDU2NGY5YzdhNGYyZTA5MTBhOTQzMzY3M2Q4YiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODY1NDcyLCJkaWdlc3QiOiJzaGEyNTY6ZTJlYjA2ZDhhZjgyMThjZmVjODIxMDE0NzM1N2E2OGI3ZTEzZjdjNDg1Yjk5MWMyODhjMmQwMWRjMjI4YmI2OCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM2NTE1MzI4LCJkaWdlc3QiOiJzaGEyNTY6NmNjNmRiMTc2NDQwZTNkYzMyMThkMmUzMjU3MTZjMTkyMmVhOWQ5MDBiNjFkN2FkNmYzODhmZDBlZDJiNGVmOSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1Njo1ZDUwMDdmMDA5YmI2MTUyMjhkYjQwNDZkNWNhZTkxMDU2Mzg1OWQxZTNhMzdjYWRiMmQ2OTFlYTc4M2FkOGE3In1dfQ==", "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6YTUyNzg0NzAxODkzMmE0ZWZlMWYxM2U4MzY3NTE4YzQ0MmI2MzE1OTA3YTE2MDRiZWJhYTJhZjg1NjgwMTc1MSIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApIENPUFkgZmlsZTo4ZTY2YzA3MmFjYjU4ZTVjN2ViZWU5MGI0ZGVhNjc1YjdjM2VmOTA5MTQ0Yjk3MzA4MzYwMGU3N2NkNDIyNzY5IGluIC8gIl0sIkltYWdlIjoic2hhMjU2OmE1Mjc4NDcwMTg5MzJhNGVmZTFmMTNlODM2NzUxOGM0NDJiNjMxNTkwN2ExNjA0YmViYWEyYWY4NTY4MDE3NTEiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMjJUMTc6MDY6MzIuOTIxOTkxNjI5WiIsImRvY2tlcl92ZXJzaW9uIjoiMjAuMTAuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA4LTI3VDE3OjE5OjQ1LjU1MzA5MjM2M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YWFkNDI5MGQyNzU4MGNjMWEwOTRmZmFmOThjM2NhMmZjNWQ2OTlmZTY5NWRmYjhlNmU5ZmFjMjBmMTEyOTQ1MCBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDgtMjdUMTc6MTk6NDUuNzU4NjExNTIzWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMS0xMC0yMlQxNzowNjozMi43MTI3NTA5MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgd2dldCAtbnYgaHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL2p1bml0L2p1bml0LzQuMTMuMS9qdW5pdC00LjEzLjEuamFyIFx1MDAyNlx1MDAyNiAgICAgd2dldCAtbnYgaHR0cHM6Ly9nZXQuamVua2lucy5pby9wbHVnaW5zL1R3aWxpb05vdGlmaWVyLzAuMi4xL1R3aWxpb05vdGlmaWVyLmhwaSBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vdXBkYXRlcy5qZW5raW5zLWNpLm9yZy9kb3dubG9hZC93YXIvMS4zOTAvaHVkc29uLndhciBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vZ2V0LmplbmtpbnMuaW8vcGx1Z2lucy9ub21hZC8wLjcuNC9ub21hZC5ocGkifSx7ImNyZWF0ZWQiOiIyMDIxLTEwLTIyVDE3OjA2OjMyLjkyMTk5MTYyOVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOjhlNjZjMDcyYWNiNThlNWM3ZWJlZTkwYjRkZWE2NzViN2MzZWY5MDkxNDRiOTczMDgzNjAwZTc3Y2Q0MjI3NjkgaW4gLyAifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjplMmViMDZkOGFmODIxOGNmZWM4MjEwMTQ3MzU3YTY4YjdlMTNmN2M0ODViOTkxYzI4OGMyZDAxZGMyMjhiYjY4Iiwic2hhMjU2OjZjYzZkYjE3NjQ0MGUzZGMzMjE4ZDJlMzI1NzE2YzE5MjJlYTlkOTAwYjYxZDdhZDZmMzg4ZmQwZWQyYjRlZjkiLCJzaGEyNTY6NWQ1MDA3ZjAwOWJiNjE1MjI4ZGI0MDQ2ZDVjYWU5MTA1NjM4NTlkMWUzYTM3Y2FkYjJkNjkxZWE3ODNhZDhhNyJdfX0=", "repoDigests": [ "anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da" ], "architecture": "amd64", "os": "linux" } }, "distro": { "name": "alpine", "version": "3.14.2", "idLike": [] }, "descriptor": { "name": "grype", "version": "0.65.1", "configuration": { "configPath": "", "verbosity": 0, "output": [ "json" ], "file": "", "distro": "", "add-cpes-if-none": false, "output-template-file": "", "check-for-app-update": true, "only-fixed": false, "only-notfixed": false, "platform": "", "search": { "scope": "Squashed", "unindexed-archives": false, "indexed-archives": true }, "ignore": null, "exclude": [], "db": { "cache-dir": "/Users/willmurphy/Library/Caches/grype/db", "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", "ca-cert": "", "auto-update": true, "validate-by-hash-on-start": false, "validate-age": true, "max-allowed-built-age": 432000000000000 }, "externalSources": { "enable": false, "maven": { "searchUpstreamBySha1": true, "baseUrl": "https://search.maven.org/solrsearch/select" } }, "match": { "java": { "using-cpes": true }, "dotnet": { "using-cpes": true }, "golang": { "using-cpes": true }, "javascript": { "using-cpes": false }, "python": { "using-cpes": true }, "ruby": { "using-cpes": true }, "stock": { "using-cpes": true } }, "dev": { "profile-cpu": false, "profile-mem": false }, "fail-on-severity": "", "registry": { "insecure-skip-tls-verify": false, "insecure-use-http": false, "auth": [] }, "log": { "quiet": true, "verbosity": 0, "level": "", "file": "" }, "show-suppressed": false, "by-cve": false, "name": "", "default-image-pull-source": "" }, "db": { "built": "2023-08-31T01:24:19Z", "schemaVersion": 5, "location": "/Users/willmurphy/Library/Caches/grype/db/5", "checksum": "sha256:911c05ea7c2a5f993758e5428c614914384c2a8265d7e2b0edb843799d62626c", "error": null }, "timestamp": "2023-08-31T15:13:32.377177-04:00" } } ================================================ FILE: grype/presenter/explain/testdata/keycloak-test.json ================================================ { "matches": [{ "vulnerability": { "id": "CVE-2020-12413", "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", "namespace": "redhat:distro:redhat:9", "severity": "Low", "urls": [ "https://access.redhat.com/security/cve/CVE-2020-12413" ], "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": { "base_severity": "Medium", "status": "draft" } } ], "fix": { "versions": [], "state": "wont-fix" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2020-12413", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", "https://raccoon-attack.com/" ], "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "rpm-matcher", "searchedBy": { "distro": { "type": "redhat", "version": "9.1" }, "namespace": "redhat:distro:redhat:9", "package": { "name": "nss", "version": "3.79.0-17.el9_1" } }, "found": { "versionConstraint": "none (rpm)", "vulnerabilityID": "CVE-2020-12413" } } ], "artifact": { "id": "ff2aefb138ebd4bf", "name": "nspr", "version": "4.34.0-17.el9_1", "type": "rpm", "locations": [ { "path": "/var/lib/rpm/rpmdb.sqlite", "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" } ], "language": "", "licenses": [ "MPLv2.0" ], "cpes": [ "cpe:2.3:a:redhat:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nspr:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*" ], "purl": "pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", "upstreams": [ { "name": "nss", "version": "3.79.0-17.el9_1" } ], "metadataType": "RpmMetadata", "metadata": { "epoch": null, "modularityLabel": "" } } }, { "vulnerability": { "id": "CVE-2020-12413", "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", "namespace": "redhat:distro:redhat:9", "severity": "Low", "urls": [ "https://access.redhat.com/security/cve/CVE-2020-12413" ], "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": { "base_severity": "Medium", "status": "draft" } } ], "fix": { "versions": [], "state": "wont-fix" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2020-12413", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", "https://raccoon-attack.com/" ], "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-direct-match", "matcher": "rpm-matcher", "searchedBy": { "distro": { "type": "redhat", "version": "9.1" }, "namespace": "redhat:distro:redhat:9", "package": { "name": "nss", "version": "0:3.79.0-17.el9_1" } }, "found": { "versionConstraint": "none (rpm)", "vulnerabilityID": "CVE-2020-12413" } } ], "artifact": { "id": "840f8a931c86688f", "name": "nss", "version": "3.79.0-17.el9_1", "type": "rpm", "locations": [ { "path": "/var/lib/rpm/rpmdb.sqlite", "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" } ], "language": "", "licenses": [ "MPLv2.0" ], "cpes": [ "cpe:2.3:a:redhat:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*" ], "purl": "pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", "upstreams": [], "metadataType": "RpmMetadata", "metadata": { "epoch": null, "modularityLabel": "" } } }, { "vulnerability": { "id": "CVE-2020-12413", "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", "namespace": "redhat:distro:redhat:9", "severity": "Low", "urls": [ "https://access.redhat.com/security/cve/CVE-2020-12413" ], "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": { "base_severity": "Medium", "status": "draft" } } ], "fix": { "versions": [], "state": "wont-fix" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2020-12413", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", "https://raccoon-attack.com/" ], "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "rpm-matcher", "searchedBy": { "distro": { "type": "redhat", "version": "9.1" }, "namespace": "redhat:distro:redhat:9", "package": { "name": "nss", "version": "3.79.0-17.el9_1" } }, "found": { "versionConstraint": "none (rpm)", "vulnerabilityID": "CVE-2020-12413" } } ], "artifact": { "id": "7d1c659d9eb00024", "name": "nss-softokn", "version": "3.79.0-17.el9_1", "type": "rpm", "locations": [ { "path": "/var/lib/rpm/rpmdb.sqlite", "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" } ], "language": "", "licenses": [ "MPLv2.0" ], "cpes": [ "cpe:2.3:a:nss-softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss-softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*" ], "purl": "pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", "upstreams": [ { "name": "nss", "version": "3.79.0-17.el9_1" } ], "metadataType": "RpmMetadata", "metadata": { "epoch": null, "modularityLabel": "" } } }, { "vulnerability": { "id": "CVE-2020-12413", "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", "namespace": "redhat:distro:redhat:9", "severity": "Low", "urls": [ "https://access.redhat.com/security/cve/CVE-2020-12413" ], "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": { "base_severity": "Medium", "status": "draft" } } ], "fix": { "versions": [], "state": "wont-fix" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2020-12413", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", "https://raccoon-attack.com/" ], "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "rpm-matcher", "searchedBy": { "distro": { "type": "redhat", "version": "9.1" }, "namespace": "redhat:distro:redhat:9", "package": { "name": "nss", "version": "3.79.0-17.el9_1" } }, "found": { "versionConstraint": "none (rpm)", "vulnerabilityID": "CVE-2020-12413" } } ], "artifact": { "id": "cb1f96627e29924e", "name": "nss-softokn-freebl", "version": "3.79.0-17.el9_1", "type": "rpm", "locations": [ { "path": "/var/lib/rpm/rpmdb.sqlite", "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" } ], "language": "", "licenses": [ "MPLv2.0" ], "cpes": [ "cpe:2.3:a:nss-softokn-freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss-softokn-freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_softokn_freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_softokn_freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss-softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss-softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*" ], "purl": "pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", "upstreams": [ { "name": "nss", "version": "3.79.0-17.el9_1" } ], "metadataType": "RpmMetadata", "metadata": { "epoch": null, "modularityLabel": "" } } }, { "vulnerability": { "id": "CVE-2020-12413", "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", "namespace": "redhat:distro:redhat:9", "severity": "Low", "urls": [ "https://access.redhat.com/security/cve/CVE-2020-12413" ], "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": { "base_severity": "Medium", "status": "draft" } } ], "fix": { "versions": [], "state": "wont-fix" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2020-12413", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", "https://raccoon-attack.com/" ], "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "rpm-matcher", "searchedBy": { "distro": { "type": "redhat", "version": "9.1" }, "namespace": "redhat:distro:redhat:9", "package": { "name": "nss", "version": "3.79.0-17.el9_1" } }, "found": { "versionConstraint": "none (rpm)", "vulnerabilityID": "CVE-2020-12413" } } ], "artifact": { "id": "d096d490e4fccf36", "name": "nss-sysinit", "version": "3.79.0-17.el9_1", "type": "rpm", "locations": [ { "path": "/var/lib/rpm/rpmdb.sqlite", "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" } ], "language": "", "licenses": [ "MPLv2.0" ], "cpes": [ "cpe:2.3:a:nss-sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss-sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*" ], "purl": "pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", "upstreams": [ { "name": "nss", "version": "3.79.0-17.el9_1" } ], "metadataType": "RpmMetadata", "metadata": { "epoch": null, "modularityLabel": "" } } }, { "vulnerability": { "id": "CVE-2020-12413", "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", "namespace": "redhat:distro:redhat:9", "severity": "Low", "urls": [ "https://access.redhat.com/security/cve/CVE-2020-12413" ], "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", "cvss": [ { "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": { "base_severity": "Medium", "status": "draft" } } ], "fix": { "versions": [], "state": "wont-fix" }, "advisories": [] }, "relatedVulnerabilities": [ { "id": "CVE-2020-12413", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", "namespace": "nvd:cpe", "severity": "Medium", "urls": [ "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", "https://raccoon-attack.com/" ], "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", "cvss": [ { "source": "nvd@nist.gov", "type": "Primary", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "metrics": { "baseScore": 5.9, "exploitabilityScore": 2.2, "impactScore": 3.6 }, "vendorMetadata": {} } ] } ], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "rpm-matcher", "searchedBy": { "distro": { "type": "redhat", "version": "9.1" }, "namespace": "redhat:distro:redhat:9", "package": { "name": "nss", "version": "3.79.0-17.el9_1" } }, "found": { "versionConstraint": "none (rpm)", "vulnerabilityID": "CVE-2020-12413" } } ], "artifact": { "id": "641950c22b3f5035", "name": "nss-util", "version": "3.79.0-17.el9_1", "type": "rpm", "locations": [ { "path": "/var/lib/rpm/rpmdb.sqlite", "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" } ], "language": "", "licenses": [ "MPLv2.0" ], "cpes": [ "cpe:2.3:a:nss-util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss-util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss_util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:redhat:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", "cpe:2.3:a:nss:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*" ], "purl": "pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", "upstreams": [ { "name": "nss", "version": "3.79.0-17.el9_1" } ], "metadataType": "RpmMetadata", "metadata": { "epoch": null, "modularityLabel": "" } } } ], "source": { "type": "image", "target": { "userInput": "docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50", "imageID": "sha256:8cf8fd2be2ded92962d52adff75ad06a4c30f69c66facbdf223364c6c9e33b8c", "manifestDigest": "sha256:d1630d3eb8285a978301bcefc5b223e564ae300750af2fc9ea3f413c5376a47e", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [], "imageSize": 433836339, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:4e37aeaccb4c8016e381d6e2b2a0f22ea59985a7b9b8eca674726e8c60f2f51d", "size": 24302817 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b", "size": 222622091 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:8329e422b4fd63ffd06518346e5f1f7b33e8190a79e5c321f9c50aba8651d30c", "size": 186910556 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:987fd030ab2cd62944cb487df846c312b64dbb2a6a3131a81253c15a9da2a26c", "size": 875 } ], "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NTY5LCJkaWdlc3QiOiJzaGEyNTY6OGNmOGZkMmJlMmRlZDkyOTYyZDUyYWRmZjc1YWQwNmE0YzMwZjY5YzY2ZmFjYmRmMjIzMzY0YzZjOWUzM2I4YyJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyNjEyNzg3MiwiZGlnZXN0Ijoic2hhMjU2OjRlMzdhZWFjY2I0YzgwMTZlMzgxZDZlMmIyYTBmMjJlYTU5OTg1YTdiOWI4ZWNhNjc0NzI2ZThjNjBmMmY1MWQifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMjUxMTI1NzYsImRpZ2VzdCI6InNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MTg3MzE1MjAwLCJkaWdlc3QiOiJzaGEyNTY6ODMyOWU0MjJiNGZkNjNmZmQwNjUxODM0NmU1ZjFmN2IzM2U4MTkwYTc5ZTVjMzIxZjljNTBhYmE4NjUxZDMwYyJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQwOTYsImRpZ2VzdCI6InNoYTI1Njo5ODdmZDAzMGFiMmNkNjI5NDRjYjQ4N2RmODQ2YzMxMmI2NGRiYjJhNmEzMTMxYTgxMjUzYzE1YTlkYTJhMjZjIn1dfQ==", "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJVc2VyIjoiMTAwMCIsIkV4cG9zZWRQb3J0cyI6eyI4MDgwL3RjcCI6e30sIjg0NDMvdGNwIjp7fX0sIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIkxBTkc9ZW5fVVMuVVRGLTgiXSwiRW50cnlwb2ludCI6WyIvb3B0L2tleWNsb2FrL2Jpbi9rYy5zaCJdLCJMYWJlbHMiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiYnVpbGQtZGF0ZSI6IjIwMjMtMDItMjJUMTM6NTQ6MjUiLCJjb20ucmVkaGF0LmNvbXBvbmVudCI6InViaTktbWljcm8tY29udGFpbmVyIiwiY29tLnJlZGhhdC5saWNlbnNlX3Rlcm1zIjoiaHR0cHM6Ly93d3cucmVkaGF0LmNvbS9lbi9hYm91dC9yZWQtaGF0LWVuZC11c2VyLWxpY2Vuc2UtYWdyZWVtZW50cyNVQkkiLCJkZXNjcmlwdGlvbiI6IlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuIiwiZGlzdHJpYnV0aW9uLXNjb3BlIjoicHVibGljIiwiaW8uYnVpbGRhaC52ZXJzaW9uIjoiMS4yNy4zIiwiaW8uazhzLmRlc2NyaXB0aW9uIjoiVmVyeSBzbWFsbCBpbWFnZSB3aGljaCBkb2Vzbid0IGluc3RhbGwgdGhlIHBhY2thZ2UgbWFuYWdlci4iLCJpby5rOHMuZGlzcGxheS1uYW1lIjoiVWJpOS1taWNybyIsImlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXMiOiIiLCJtYWludGFpbmVyIjoiUmVkIEhhdCwgSW5jLiIsIm5hbWUiOiJ1Ymk5L3ViaS1taWNybyIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5jcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMjoxOC45ODVaIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uIjoiIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzIjoiQXBhY2hlLTIuMCIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5yZXZpc2lvbiI6ImIzNTJhMWY2ZThiYTkyYTA0NWI1OWNjOGRlZDE4NWUzYjFkMjYxNTUiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGUiOiJrZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVyc2lvbiI6IjIxLjAuMiIsInJlbGVhc2UiOiIxNSIsInN1bW1hcnkiOiJ1Ymk5IG1pY3JvIGltYWdlIiwidXJsIjoiaHR0cHM6Ly9hY2Nlc3MucmVkaGF0LmNvbS9jb250YWluZXJzLyMvcmVnaXN0cnkuYWNjZXNzLnJlZGhhdC5jb20vdWJpOS91YmktbWljcm8vaW1hZ2VzLzkuMS4wLTE1IiwidmNzLXJlZiI6ImM1NjNlMDkxZTBjN2JkNWE2OWIyYTQ2OTkwZGRhNGY1OTU5NWFhMzciLCJ2Y3MtdHlwZSI6ImdpdCIsInZlbmRvciI6IlJlZCBIYXQsIEluYy4iLCJ2ZXJzaW9uIjoiOS4xLjAifSwiT25CdWlsZCI6bnVsbH0sImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc2NTUxODNaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIG1haW50YWluZXI9XCJSZWQgSGF0LCBJbmMuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc3ODIxNDlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGNvbS5yZWRoYXQuY29tcG9uZW50PVwidWJpOS1taWNyby1jb250YWluZXJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjAzLjkzNzgwODc4M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgbmFtZT1cInViaTkvdWJpLW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc4NTM2MzVaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHZlcnNpb249XCI5LjEuMFwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTMwMjE1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBjb20ucmVkaGF0LmxpY2Vuc2VfdGVybXM9XCJodHRwczovL3d3dy5yZWRoYXQuY29tL2VuL2Fib3V0L3JlZC1oYXQtZW5kLXVzZXItbGljZW5zZS1hZ3JlZW1lbnRzI1VCSVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTY5MTY5WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBzdW1tYXJ5PVwidWJpOSBtaWNybyBpbWFnZVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM4MDAzNDAyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBkZXNjcmlwdGlvbj1cIlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgyMjU4ODRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLms4cy5kaXNwbGF5LW5hbWU9XCJVYmk5LW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgzMjAxOTlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXM9XCJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA0LjkxOTA1OTE2OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBkaXI6MWMzNjc0ODA2NTg5ZWRhYmNiNTg1NGIxOThhMjlkZGU3ODZhYmI3MmU4YTU5NTA4OGRmNTI3Mjg1MzVjMTk5ZiBpbiAvICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1LjE4OTA4MTIyM1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOmVlYzczZjg1OWM2ZTdmNmM4YTk0MjdlY2M1MjQ5NTA0ZmU4OWFlNTRkYzNhMTUyMWI0NDI2NzRhOTA0OTdkMzIgaW4gL2V0Yy95dW0ucmVwb3MuZC91YmkucmVwbyAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS4xODkxMTM0MDRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIENNRCAvYmluL3NoIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuMTg5MTkyNDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHJlbGVhc2U9MTUiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS40NTY0OTQ0MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjI0ODgwMDQ4ZTQ5YzMwZDJlNWQ2ZGQ3M2VmNzg5NGU2OTIyMTczN2M5MmM0YWNjNDgxYWMxNmY1NDZjZWE3OTEgaW4gL3Jvb3QvYnVpbGRpbmZvL2NvbnRlbnRfbWFuaWZlc3RzL3ViaTktbWljcm8tY29udGFpbmVyLTkuMS4wLTE1Lmpzb24gIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuNzM0NTA1MjUyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTpjYTYxZjYyNzUwMTQ4MWFlZjYyNzcwZjM4ZmY0NjQyNzY2MzcwZDBiYjE2ZjM0MzBlZjI4NzkyNzcxMDAwYjE0IGluIC9yb290L2J1aWxkaW5mby9Eb2NrZXJmaWxlLXViaTktdWJpLW1pY3JvLTkuMS4wLTE1ICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1Ljk3NjcyMjE4OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgXCJkaXN0cmlidXRpb24tc2NvcGVcIj1cInB1YmxpY1wiIFwidmVuZG9yXCI9XCJSZWQgSGF0LCBJbmMuXCIgXCJidWlsZC1kYXRlXCI9XCIyMDIzLTAyLTIyVDEzOjU0OjI1XCIgXCJhcmNoaXRlY3R1cmVcIj1cIng4Nl82NFwiIFwidmNzLXR5cGVcIj1cImdpdFwiIFwidmNzLXJlZlwiPVwiYzU2M2UwOTFlMGM3YmQ1YTY5YjJhNDY5OTBkZGE0ZjU5NTk1YWEzN1wiIFwiaW8uazhzLmRlc2NyaXB0aW9uXCI9XCJWZXJ5IHNtYWxsIGltYWdlIHdoaWNoIGRvZXNuJ3QgaW5zdGFsbCB0aGUgcGFja2FnZSBtYW5hZ2VyLlwiIFwidXJsXCI9XCJodHRwczovL2FjY2Vzcy5yZWRoYXQuY29tL2NvbnRhaW5lcnMvIy9yZWdpc3RyeS5hY2Nlc3MucmVkaGF0LmNvbS91Ymk5L3ViaS1taWNyby9pbWFnZXMvOS4xLjAtMTVcIiJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJFTlYgTEFORz1lbl9VUy5VVEYtOCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJDT1BZIC90bXAvbnVsbC9yb290ZnMvIC8gIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC41NDg0MDQ5NzZaIiwiY3JlYXRlZF9ieSI6IkNPUFkgL29wdC9rZXljbG9hayAvb3B0L2tleWNsb2FrICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTQuNjI5OTY2MDk0WiIsImNyZWF0ZWRfYnkiOiJSVU4gL2Jpbi9zaCAtYyBlY2hvIFwia2V5Y2xvYWs6eDowOnJvb3RcIiBcdTAwM2VcdTAwM2UgL2V0Yy9ncm91cCBcdTAwMjZcdTAwMjYgICAgIGVjaG8gXCJrZXljbG9hazp4OjEwMDA6MDprZXljbG9hayB1c2VyOi9vcHQva2V5Y2xvYWs6L3NiaW4vbm9sb2dpblwiIFx1MDAzZVx1MDAzZSAvZXRjL3Bhc3N3ZCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJjcmVhdGVkX2J5IjoiVVNFUiAxMDAwIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODA4MC90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODQ0My90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVOVFJZUE9JTlQgW1wiL29wdC9rZXljbG9hay9iaW4va2Muc2hcIl0iLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfV0sIm1vYnkuYnVpbGRraXQuYnVpbGRpbmZvLnYxIjoiZXlKbWNtOXVkR1Z1WkNJNkltUnZZMnRsY21acGJHVXVkakFpTENKemIzVnlZMlZ6SWpwYmV5SjBlWEJsSWpvaVpHOWphMlZ5TFdsdFlXZGxJaXdpY21WbUlqb2ljbVZuYVhOMGNua3VZV05qWlhOekxuSmxaR2hoZEM1amIyMHZkV0pwT1MxdGFXTnlienBzWVhSbGMzUWlMQ0p3YVc0aU9pSnphR0V5TlRZNk5UTTJOemszTVRRNVlqSmxOakUwWXpFMll6RTRaRE00WVdVM1pqVXdPRGc1TWprM1pUa3lZVGRqWXpSaE5qQXlORGt4TkRjM1pHUmpNMkppTURZeFppSjlMSHNpZEhsd1pTSTZJbVJ2WTJ0bGNpMXBiV0ZuWlNJc0luSmxaaUk2SW5KbFoybHpkSEo1TG1GalkyVnpjeTV5WldSb1lYUXVZMjl0TDNWaWFUazZiR0YwWlhOMElpd2ljR2x1SWpvaWMyaGhNalUyT21OaU16QXpOREEwWlRVM05tWm1OVFV5T0dRMFpqQTRZakV5WVdRNE5XWmhZamhtTmpGbVlUbGxOV1JpWVRZM1lqTTNZakV4T1dSaU1qUTROalZrWmpNaWZWMTkiLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NGUzN2FlYWNjYjRjODAxNmUzODFkNmUyYjJhMGYyMmVhNTk5ODVhN2I5YjhlY2E2NzQ3MjZlOGM2MGYyZjUxZCIsInNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIiwic2hhMjU2OjgzMjllNDIyYjRmZDYzZmZkMDY1MTgzNDZlNWYxZjdiMzNlODE5MGE3OWU1YzMyMWY5YzUwYWJhODY1MWQzMGMiLCJzaGEyNTY6OTg3ZmQwMzBhYjJjZDYyOTQ0Y2I0ODdkZjg0NmMzMTJiNjRkYmIyYTZhMzEzMWE4MTI1M2MxNWE5ZGEyYTI2YyJdfX0=", "repoDigests": [ "keycloak/keycloak@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50" ], "architecture": "amd64", "os": "linux" } }, "distro": { "name": "redhat", "version": "9.1", "idLike": [ "fedora" ] }, "descriptor": { "name": "grype", "version": "0.66.0", "configuration": { "configPath": "", "verbosity": 0, "output": [ "json" ], "file": "", "distro": "", "add-cpes-if-none": false, "output-template-file": "", "check-for-app-update": true, "only-fixed": false, "only-notfixed": false, "platform": "", "search": { "scope": "Squashed", "unindexed-archives": false, "indexed-archives": true }, "ignore": null, "exclude": [], "db": { "cache-dir": "/Users/willmurphy/Library/Caches/grype/db", "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", "ca-cert": "", "auto-update": true, "validate-by-hash-on-start": false, "validate-age": true, "max-allowed-built-age": 432000000000000 }, "externalSources": { "enable": false, "maven": { "searchUpstreamBySha1": true, "baseUrl": "https://search.maven.org/solrsearch/select" } }, "match": { "java": { "using-cpes": true }, "dotnet": { "using-cpes": true }, "golang": { "using-cpes": true }, "javascript": { "using-cpes": false }, "python": { "using-cpes": true }, "ruby": { "using-cpes": true }, "stock": { "using-cpes": true } }, "dev": { "profile-cpu": false, "profile-mem": false }, "fail-on-severity": "", "registry": { "insecure-skip-tls-verify": false, "insecure-use-http": false, "auth": [], "ca-cert": "" }, "log": { "quiet": true, "verbosity": 0, "level": "", "file": "" }, "show-suppressed": false, "by-cve": false, "name": "", "default-image-pull-source": "" }, "db": { "built": "2023-09-01T01:26:55Z", "schemaVersion": 5, "location": "/Users/willmurphy/Library/Caches/grype/db/5", "checksum": "sha256:5db8bddae95f375db7186527c7554311e9ddc41e815ef2dbc28dc5d206ef2c7b", "error": null }, "timestamp": "2023-09-01T08:13:42.20194-04:00" } } ================================================ FILE: grype/presenter/internal/test_helpers.go ================================================ package internal import ( "regexp" "testing" "github.com/stretchr/testify/require" "github.com/anchore/clio" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" vexStatus "github.com/anchore/grype/grype/vex/status" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" syftSource "github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/directorysource" "github.com/anchore/syft/syft/source/filesource" "github.com/anchore/syft/syft/source/stereoscopesource" ) const ( DirectorySource SyftSource = "directory" ImageSource SyftSource = "image" FileSource SyftSource = "file" ) type SyftSource string func GeneratePresenterConfig(t *testing.T, scheme SyftSource) models.PresenterConfig { s, doc := GenerateAnalysis(t, scheme) return models.PresenterConfig{ ID: clio.Identification{Name: "grype", Version: "[not provided]"}, Document: doc, SBOM: s, Pretty: true, } } func GenerateAnalysis(t *testing.T, scheme SyftSource) (*sbom.SBOM, models.Document) { t.Helper() context := generateContext(t, scheme) s := &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: syftPkg.NewCollection(generatePackages(t)...), }, Source: *context.Source, } grypePackages := pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{}) matches := generateMatches(t, grypePackages[0], grypePackages[1]) doc, err := models.NewDocument(clio.Identification{Name: "grype", Version: "[not provided]"}, grypePackages, context, matches, nil, models.NewMetadataMock(), nil, nil, models.SortByPackage, true, nil) require.NoError(t, err) return s, doc } func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme SyftSource) models.Document { t.Helper() s := &sbom.SBOM{ Artifacts: sbom.Artifacts{ Packages: syftPkg.NewCollection(generatePackages(t)...), }, } grypePackages := pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{}) matches := generateMatches(t, grypePackages[0], grypePackages[1]) ignoredMatches := generateIgnoredMatches(t, grypePackages[1]) context := generateContext(t, scheme) doc, err := models.NewDocument(clio.Identification{Name: "grype", Version: "devel"}, grypePackages, context, matches, ignoredMatches, models.NewMetadataMock(), nil, nil, models.SortByPackage, true, nil) require.NoError(t, err) return doc } func Redact(s []byte) []byte { serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`) uuidPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) refPattern := regexp.MustCompile(`ref="[a-zA-Z0-9\-:]+"`) rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`) cycloneDxBomRefPattern := regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) tempDirPattern := regexp.MustCompile(`/tmp/[^"]+`) macTempDirPattern := regexp.MustCompile(`/var/folders/[^"]+`) for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern, tempDirPattern, macTempDirPattern} { s = pattern.ReplaceAll(s, []byte("")) } return s } func generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { // nolint:funlen t.Helper() matches := []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1999-0001", Namespace: "source-1", }, Fix: vulnerability.Fix{ Versions: []string{"1.2.1", "2.1.3", "3.4.0"}, State: vulnerability.FixStateFixed, }, Metadata: &vulnerability.Metadata{ ID: "CVE-1999-0001", Severity: "Low", Cvss: []vulnerability.Cvss{ { Source: "nvd", Type: "CVSS", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 8.2, }, }, }, KnownExploited: nil, EPSS: []vulnerability.EPSS{ { CVE: "CVE-1999-0001", EPSS: 0.03, Percentile: 0.42, }, }, }, }, Package: p1, Details: []match.Detail{ { Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": "ubuntu", "version": "20.04", }, }, Found: map[string]interface{}{ "constraint": ">= 20", }, }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1999-0002", Namespace: "source-2", }, Metadata: &vulnerability.Metadata{ ID: "CVE-1999-0002", Severity: "Critical", Cvss: []vulnerability.Cvss{ { Source: "nvd", Type: "CVSS", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 8.5, }, }, }, KnownExploited: []vulnerability.KnownExploited{ { CVE: "CVE-1999-0002", KnownRansomwareCampaignUse: "Known", }, }, EPSS: []vulnerability.EPSS{ { CVE: "CVE-1999-0002", EPSS: 0.08, Percentile: 0.53, }, }, }, }, Package: p2, Details: []match.Detail{ { Type: match.ExactIndirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "cpe": "somecpe", }, Found: map[string]interface{}{ "constraint": "somecpe", }, }, }, }, } collection := match.NewMatches(matches...) return collection } // nolint: funlen func generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch { t.Helper() return []match.IgnoredMatch{ { Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1999-0001", Namespace: "source-1", }, Metadata: &vulnerability.Metadata{ ID: "CVE-1999-0001", Severity: "Low", Cvss: []vulnerability.Cvss{ { Source: "nvd", Type: "CVSS", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 8.2, }, }, }, KnownExploited: nil, EPSS: []vulnerability.EPSS{ { CVE: "CVE-1999-0001", EPSS: 0.03, Percentile: 0.42, }, }, }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "distro": map[string]string{ "type": "ubuntu", "version": "20.04", }, }, Found: map[string]interface{}{ "constraint": ">= 20", }, }, }, }, AppliedIgnoreRules: []match.IgnoreRule{}, }, { Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1999-0002", Namespace: "source-2", }, Metadata: &vulnerability.Metadata{ ID: "CVE-1999-0002", Severity: "Critical", Cvss: []vulnerability.Cvss{ { Source: "nvd", Type: "CVSS", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", Metrics: vulnerability.CvssMetrics{ BaseScore: 8.5, }, }, }, KnownExploited: []vulnerability.KnownExploited{ { CVE: "CVE-1999-0002", KnownRansomwareCampaignUse: "Known", }, }, EPSS: []vulnerability.EPSS{ { CVE: "CVE-1999-0002", EPSS: 0.08, Percentile: 0.53, }, }, }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "cpe": "somecpe", }, Found: map[string]interface{}{ "constraint": "somecpe", }, }, }, }, AppliedIgnoreRules: []match.IgnoreRule{}, }, { Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-1999-0004", Namespace: "source-2", }, Metadata: &vulnerability.Metadata{ ID: "CVE-1999-0004", Severity: "High", Cvss: []vulnerability.Cvss{ { Source: "nvd", Type: "CVSS", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:L", Metrics: vulnerability.CvssMetrics{ BaseScore: 7.2, }, }, }, EPSS: []vulnerability.EPSS{ { CVE: "CVE-1999-0004", EPSS: 0.03, Percentile: 0.75, }, }, }, }, Package: p, Details: []match.Detail{ { Type: match.ExactDirectMatch, Matcher: match.DpkgMatcher, SearchedBy: map[string]interface{}{ "cpe": "somecpe", }, Found: map[string]interface{}{ "constraint": "somecpe", }, }, }, }, AppliedIgnoreRules: []match.IgnoreRule{ { Vulnerability: "CVE-1999-0004", Namespace: "vex", Package: match.IgnoreRulePackage{}, VexStatus: string(vexStatus.NotAffected), VexJustification: "this isn't the vulnerability match you're looking for... *waves hand*", }, }, }, } } func generatePackages(t *testing.T) []syftPkg.Package { t.Helper() epoch := 2 pkgs := []syftPkg.Package{ { Name: "package-1", Version: "1.1.1", Type: syftPkg.RpmPkg, Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-1.txt", "somefile-1.txt")), CPEs: []cpe.CPE{ { Attributes: cpe.Attributes{ Part: "a", Vendor: "anchore:oss", Product: "anchore/engine", Version: "0.9.2", Language: "en", }, }, }, Metadata: syftPkg.RpmDBEntry{ Epoch: &epoch, SourceRpm: "some-source-rpm", }, }, { Name: "package-2", Version: "2.2.2", Type: syftPkg.DebPkg, PURL: "pkg:deb/package-2@2.2.2", Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-2.txt", "somefile-2.txt")), CPEs: []cpe.CPE{ { Attributes: cpe.Attributes{ Part: "a", Vendor: "anchore", Product: "engine", Version: "2.2.2", Language: "en", }, }, }, Licenses: syftPkg.NewLicenseSet( syftPkg.NewLicense("MIT"), syftPkg.NewLicense("Apache-2.0"), ), }, } for i := range pkgs { p := pkgs[i] p.SetID() } return pkgs } //nolint:funlen func generateContext(t *testing.T, scheme SyftSource) pkg.Context { var ( src syftSource.Source desc syftSource.Description ) switch scheme { case FileSource: var err error src, err = filesource.NewFromPath("user-input") if err != nil { t.Fatalf("failed to generate mock file source from mock image: %+v", err) } desc = src.Describe() case ImageSource: img := image.Image{ Metadata: image.Metadata{ ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", MediaType: "application/vnd.docker.distribution.manifest.v2+json", Size: 65, }, Layers: []*image.Layer{ { Metadata: image.LayerMetadata{ Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 22, }, }, { Metadata: image.LayerMetadata{ Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 16, }, }, { Metadata: image.LayerMetadata{ Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 27, }, }, }, } src = stereoscopesource.New(&img, stereoscopesource.ImageConfig{ Reference: "user-input", }) desc = src.Describe() case DirectorySource: // note: the dir must exist for the source to be created d := t.TempDir() var err error src, err = directorysource.NewFromPath(d) if err != nil { t.Fatalf("failed to generate mock directory source from mock dir: %+v", err) } desc = src.Describe() if m, ok := desc.Metadata.(syftSource.DirectoryMetadata); ok { m.Path = "/some/path" desc.Metadata = m } default: t.Fatalf("unknown scheme: %s", scheme) } return pkg.Context{ Source: &desc, Distro: &distro.Distro{ Type: "centos", IDLike: []string{ "centos", }, Version: "8.0", Channels: []string{"eus"}, // a fake EUS-like channel for centOS }, } } ================================================ FILE: grype/presenter/json/presenter.go ================================================ package json //nolint:revive import ( "encoding/json" "io" "github.com/anchore/grype/grype/presenter/models" ) type Presenter struct { document models.Document pretty bool } func NewPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ document: pb.Document, pretty: pb.Pretty, } } func (p *Presenter) Present(output io.Writer) error { enc := json.NewEncoder(output) // prevent > and < from being escaped in the payload enc.SetEscapeHTML(false) if p.pretty { enc.SetIndent("", " ") } return enc.Encode(&p.document) } ================================================ FILE: grype/presenter/json/presenter_test.go ================================================ package json import ( "bytes" "flag" "regexp" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/clio" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal/testutils" "github.com/anchore/syft/syft/source" ) var update = flag.Bool("update", false, "update the *.golden files for json presenters") var timestampRegexp = regexp.MustCompile(`"timestamp":\s*"[^"]+"`) func TestJsonImgsPresenter(t *testing.T) { var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, internal.ImageSource) pres := NewPresenter(pb) // run presenter if err := pres.Present(&buffer); err != nil { t.Fatal(err) } actual := buffer.Bytes() actual = redact(actual) if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) if d := cmp.Diff(string(expected), string(actual)); d != "" { t.Fatalf("diff: %s", d) } // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) } func TestJsonDirsPresenter(t *testing.T) { var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, internal.DirectorySource) pres := NewPresenter(pb) // run presenter if err := pres.Present(&buffer); err != nil { t.Fatal(err) } actual := buffer.Bytes() actual = redact(actual) if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) if d := cmp.Diff(string(expected), string(actual)); d != "" { t.Fatalf("diff: %s", d) } // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) } func TestEmptyJsonPresenter(t *testing.T) { // expected to have an empty JSON array back var buffer bytes.Buffer ctx := pkg.Context{ Source: &source.Description{}, Distro: &distro.Distro{ Type: "centos", IDLike: []string{"rhel"}, Version: "8.0", }, } doc, err := models.NewDocument(clio.Identification{Name: "grype", Version: "[not provided]"}, nil, ctx, match.NewMatches(), nil, models.NewMetadataMock(), nil, nil, models.SortByPackage, true, nil) require.NoError(t, err) pb := models.PresenterConfig{ ID: clio.Identification{ Name: "grype", Version: "[not provided]", }, Document: doc, } pres := NewPresenter(pb) // run presenter if err := pres.Present(&buffer); err != nil { t.Fatal(err) } actual := buffer.Bytes() actual = redact(actual) if *update { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) assert.JSONEq(t, string(expected), string(actual)) } func redact(content []byte) []byte { return timestampRegexp.ReplaceAll(content, []byte(`"timestamp":""`)) } ================================================ FILE: grype/presenter/json/testdata/image-simple/Dockerfile ================================================ # Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. FROM scratch ADD file-1.txt /somefile-1.txt ADD file-2.txt /somefile-2.txt # note: adding a directory will behave differently on docker engine v18 vs v19 ADD target / ================================================ FILE: grype/presenter/json/testdata/image-simple/file-1.txt ================================================ this file has contents ================================================ FILE: grype/presenter/json/testdata/image-simple/file-2.txt ================================================ file-2 contents! ================================================ FILE: grype/presenter/json/testdata/image-simple/target/really/nested/file-3.txt ================================================ another file! with lines... ================================================ FILE: grype/presenter/json/testdata/snapshot/TestEmptyJsonPresenter.golden ================================================ {"matches":[],"source":{"type":"unknown","target":"unknown"},"distro":{"name":"centos","version":"8.0","idLike":["rhel"]},"descriptor":{"name":"grype","version":"[not provided]","timestamp":""}} ================================================ FILE: grype/presenter/json/testdata/snapshot/TestJsonDirsPresenter.golden ================================================ { "matches": [ { "vulnerability": { "id": "CVE-1999-0001", "dataSource": "", "severity": "Low", "urls": [], "cvss": [ { "source": "nvd", "type": "CVSS", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", "metrics": { "baseScore": 8.2 }, "vendorMetadata": {} } ], "epss": [ { "cve": "CVE-1999-0001", "epss": 0.03, "percentile": 0.42, "date": "0001-01-01" } ], "fix": { "versions": [ "1.2.1", "2.1.3", "3.4.0" ], "state": "fixed" }, "advisories": [], "risk": 1.68 }, "relatedVulnerabilities": [], "matchDetails": [ { "type": "exact-direct-match", "matcher": "dpkg-matcher", "searchedBy": { "distro": { "type": "ubuntu", "version": "20.04" } }, "found": { "constraint": ">= 20" }, "fix": { "suggestedVersion": "1.2.1" } } ], "artifact": { "id": "bbb0ba712c2b94ea", "name": "package-1", "version": "1.1.1", "type": "rpm", "locations": [ { "path": "/foo/bar/somefile-1.txt", "accessPath": "somefile-1.txt" } ], "language": "", "licenses": [], "cpes": [ "cpe:2.3:a:anchore\\:oss:anchore\\/engine:0.9.2:*:*:en:*:*:*:*" ], "purl": "", "upstreams": [], "metadataType": "RpmMetadata", "metadata": { "epoch": 2, "modularityLabel": null } } }, { "vulnerability": { "id": "CVE-1999-0002", "dataSource": "", "severity": "Critical", "urls": [], "cvss": [ { "source": "nvd", "type": "CVSS", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", "metrics": { "baseScore": 8.5 }, "vendorMetadata": {} } ], "knownExploited": [ { "cve": "CVE-1999-0002", "knownRansomwareCampaignUse": "Known" } ], "epss": [ { "cve": "CVE-1999-0002", "epss": 0.08, "percentile": 0.53, "date": "0001-01-01" } ], "fix": { "versions": [], "state": "" }, "advisories": [], "risk": 96.25000000000001 }, "relatedVulnerabilities": [], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "dpkg-matcher", "searchedBy": { "cpe": "somecpe" }, "found": { "constraint": "somecpe" } } ], "artifact": { "id": "74378afe15713625", "name": "package-2", "version": "2.2.2", "type": "deb", "locations": [ { "path": "/foo/bar/somefile-2.txt", "accessPath": "somefile-2.txt" } ], "language": "", "licenses": [ "Apache-2.0", "MIT" ], "cpes": [ "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*" ], "purl": "pkg:deb/package-2@2.2.2", "upstreams": [] } } ], "source": { "type": "directory", "target": "/some/path" }, "distro": { "name": "centos", "version": "8.0", "idLike": [ "centos" ], "channels": [ "eus" ] }, "descriptor": { "name": "grype", "version": "[not provided]", "timestamp":"" } } ================================================ FILE: grype/presenter/json/testdata/snapshot/TestJsonImgsPresenter.golden ================================================ { "matches": [ { "vulnerability": { "id": "CVE-1999-0001", "dataSource": "", "severity": "Low", "urls": [], "cvss": [ { "source": "nvd", "type": "CVSS", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", "metrics": { "baseScore": 8.2 }, "vendorMetadata": {} } ], "epss": [ { "cve": "CVE-1999-0001", "epss": 0.03, "percentile": 0.42, "date": "0001-01-01" } ], "fix": { "versions": [ "1.2.1", "2.1.3", "3.4.0" ], "state": "fixed" }, "advisories": [], "risk": 1.68 }, "relatedVulnerabilities": [], "matchDetails": [ { "type": "exact-direct-match", "matcher": "dpkg-matcher", "searchedBy": { "distro": { "type": "ubuntu", "version": "20.04" } }, "found": { "constraint": ">= 20" }, "fix": { "suggestedVersion": "1.2.1" } } ], "artifact": { "id": "bbb0ba712c2b94ea", "name": "package-1", "version": "1.1.1", "type": "rpm", "locations": [ { "path": "/foo/bar/somefile-1.txt", "accessPath": "somefile-1.txt" } ], "language": "", "licenses": [], "cpes": [ "cpe:2.3:a:anchore\\:oss:anchore\\/engine:0.9.2:*:*:en:*:*:*:*" ], "purl": "", "upstreams": [], "metadataType": "RpmMetadata", "metadata": { "epoch": 2, "modularityLabel": null } } }, { "vulnerability": { "id": "CVE-1999-0002", "dataSource": "", "severity": "Critical", "urls": [], "cvss": [ { "source": "nvd", "type": "CVSS", "version": "3.1", "vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H", "metrics": { "baseScore": 8.5 }, "vendorMetadata": {} } ], "knownExploited": [ { "cve": "CVE-1999-0002", "knownRansomwareCampaignUse": "Known" } ], "epss": [ { "cve": "CVE-1999-0002", "epss": 0.08, "percentile": 0.53, "date": "0001-01-01" } ], "fix": { "versions": [], "state": "" }, "advisories": [], "risk": 96.25000000000001 }, "relatedVulnerabilities": [], "matchDetails": [ { "type": "exact-indirect-match", "matcher": "dpkg-matcher", "searchedBy": { "cpe": "somecpe" }, "found": { "constraint": "somecpe" } } ], "artifact": { "id": "74378afe15713625", "name": "package-2", "version": "2.2.2", "type": "deb", "locations": [ { "path": "/foo/bar/somefile-2.txt", "accessPath": "somefile-2.txt" } ], "language": "", "licenses": [ "Apache-2.0", "MIT" ], "cpes": [ "cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*" ], "purl": "pkg:deb/package-2@2.2.2", "upstreams": [] } } ], "source": { "type": "image", "target": { "userInput": "user-input", "imageID": "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", "manifestDigest": "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [], "imageSize": 65, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", "size": 22 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", "size": 16 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", "size": 27 } ], "manifest": null, "config": null, "repoDigests": [], "architecture": "", "os": "" } }, "distro": { "name": "centos", "version": "8.0", "idLike": [ "centos" ], "channels": [ "eus" ] }, "descriptor": { "name": "grype", "version": "[not provided]", "timestamp":"" } } ================================================ FILE: grype/presenter/models/alert.go ================================================ package models import ( "github.com/anchore/grype/grype/pkg" ) // AlertType represents categories of non-vulnerability concerns type AlertType string const ( // AlertTypeDistroEOL indicates a package is from an end-of-life distro AlertTypeDistroEOL AlertType = "distro-eol" ) // Alert represents a non-vulnerability concern for a package type Alert struct { Type AlertType `json:"type"` Message string `json:"message"` Metadata any `json:"metadata,omitempty"` } // DistroAlertMetadata contains machine-readable details for distro-related alerts type DistroAlertMetadata struct { Name string `json:"name"` Version string `json:"version"` } // PackageAlerts groups alerts for a specific package type PackageAlerts struct { Package Package `json:"package"` Alerts []Alert `json:"alerts"` } // DistroAlertData holds packages that should generate distro-related alerts. // This data is typically collected during vulnerability matching and passed // to NewDocument for alert generation. type DistroAlertData struct { // EOLDistroPackages are packages from distros that have reached end-of-life EOLDistroPackages []pkg.Package } ================================================ FILE: grype/presenter/models/alert_test.go ================================================ package models import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAlertTypes(t *testing.T) { tests := []struct { name string alert AlertType expected string }{ { name: "distro EOL alert type", alert: AlertTypeDistroEOL, expected: "distro-eol", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.expected, string(tc.alert)) }) } } func TestAlertJSONSerialization(t *testing.T) { alert := Alert{ Type: AlertTypeDistroEOL, Message: "Ubuntu 18.04 reached end-of-life on 2023-05-31", Metadata: DistroAlertMetadata{ Name: "ubuntu", Version: "18.04", }, } jsonBytes, err := json.Marshal(alert) require.NoError(t, err) var result map[string]interface{} err = json.Unmarshal(jsonBytes, &result) require.NoError(t, err) assert.Equal(t, "distro-eol", result["type"]) assert.Equal(t, "Ubuntu 18.04 reached end-of-life on 2023-05-31", result["message"]) assert.NotNil(t, result["metadata"]) metadata := result["metadata"].(map[string]interface{}) assert.Equal(t, "ubuntu", metadata["name"]) assert.Equal(t, "18.04", metadata["version"]) } func TestPackageAlertsJSONSerialization(t *testing.T) { pkgAlerts := PackageAlerts{ Package: Package{ ID: "pkg-123", Name: "openssl", Version: "1.1.1", Type: "deb", }, Alerts: []Alert{ { Type: AlertTypeDistroEOL, Message: "Package is from an end-of-life distribution", }, }, } jsonBytes, err := json.Marshal(pkgAlerts) require.NoError(t, err) var result map[string]interface{} err = json.Unmarshal(jsonBytes, &result) require.NoError(t, err) pkg := result["package"].(map[string]interface{}) assert.Equal(t, "openssl", pkg["name"]) alerts := result["alerts"].([]interface{}) assert.Len(t, alerts, 1) alert := alerts[0].(map[string]interface{}) assert.Equal(t, "distro-eol", alert["type"]) } func TestAlertDetailsOmitEmpty(t *testing.T) { alert := Alert{ Type: AlertTypeDistroEOL, Message: "EOL distro", // Metadata intentionally nil } jsonBytes, err := json.Marshal(alert) require.NoError(t, err) var result map[string]interface{} err = json.Unmarshal(jsonBytes, &result) require.NoError(t, err) // Metadata should be omitted when nil _, hasMetadata := result["metadata"] assert.False(t, hasMetadata, "metadata should be omitted when nil") } ================================================ FILE: grype/presenter/models/cvss.go ================================================ package models import "github.com/anchore/grype/grype/vulnerability" type Cvss struct { Source string `json:"source,omitempty"` Type string `json:"type,omitempty"` Version string `json:"version"` Vector string `json:"vector"` Metrics CvssMetrics `json:"metrics"` VendorMetadata interface{} `json:"vendorMetadata"` } type CvssMetrics struct { BaseScore float64 `json:"baseScore"` ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` ImpactScore *float64 `json:"impactScore,omitempty"` } func toCVSS(metadata *vulnerability.Metadata) []Cvss { cvss := make([]Cvss, 0) for _, score := range metadata.Cvss { vendorMetadata := score.VendorMetadata if vendorMetadata == nil { vendorMetadata = make(map[string]interface{}) } cvss = append(cvss, Cvss{ Source: score.Source, Type: score.Type, Version: score.Version, Vector: score.Vector, Metrics: CvssMetrics{ BaseScore: score.Metrics.BaseScore, ExploitabilityScore: score.Metrics.ExploitabilityScore, ImpactScore: score.Metrics.ImpactScore, }, VendorMetadata: vendorMetadata, }) } return cvss } ================================================ FILE: grype/presenter/models/descriptor.go ================================================ package models // descriptor describes what created the document as well as surrounding metadata type descriptor struct { Name string `json:"name"` Version string `json:"version"` Configuration any `json:"configuration,omitempty"` DB any `json:"db,omitempty"` Timestamp string `json:"timestamp,omitempty"` } ================================================ FILE: grype/presenter/models/distribution.go ================================================ package models import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" ) // distribution provides information about a detected Linux distribution. type distribution struct { Name string `json:"name"` // Name of the Linux distribution Version string `json:"version"` // Version of the Linux distribution (major or major.minor version) IDLike []string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file Channels []string `json:"channels,omitempty"` // channels for the distribution, if available } // newDistribution creates a struct with the Linux distribution to be represented in JSON. func newDistribution(ctx pkg.Context, d *distro.Distro) distribution { if ctx.Distro != nil { // if the distro is provided in the context, use it d = ctx.Distro } if d == nil { return distribution{} } return distribution{ Name: d.Name(), Version: d.Version, IDLike: cleanIDLike(d.IDLike), Channels: d.Channels, } } func cleanIDLike(idLike []string) []string { if idLike == nil { return make([]string, 0) } return idLike } ================================================ FILE: grype/presenter/models/document.go ================================================ package models import ( "cmp" "fmt" "slices" "time" "github.com/anchore/clio" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" ) // Document represents the JSON document to be presented type Document struct { Matches []Match `json:"matches"` IgnoredMatches []IgnoredMatch `json:"ignoredMatches,omitempty"` AlertsByPackage []PackageAlerts `json:"alertsByPackage,omitempty"` Source *source `json:"source"` Distro distribution `json:"distro"` Descriptor descriptor `json:"descriptor"` } // NewDocument creates and populates a new Document struct, representing the populated JSON document. // //nolint:staticcheck // MetadataProvider is deprecated but still used internally func NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Context, matches match.Matches, ignoredMatches []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider, appConfig any, dbInfo any, strategy SortStrategy, outputTimestamp bool, distroAlerts *DistroAlertData) (Document, error) { timestamp, err := createTimestamp(outputTimestamp) if err != nil { return Document{}, err } // we must preallocate the findings to ensure the JSON document does not show "null" when no matches are found var findings = make([]Match, 0) for _, m := range matches.Sorted() { p := pkg.ByID(m.Package.ID, packages) if p == nil { return Document{}, fmt.Errorf("unable to find package in collection: %+v", p) } matchModel, err := newMatch(m, *p, metadataProvider) if err != nil { return Document{}, err } findings = append(findings, *matchModel) } SortMatches(findings, strategy) var src *source if context.Source != nil { theSrc, err := newSource(*context.Source) if err != nil { return Document{}, err } src = &theSrc } var ignoredMatchModels []IgnoredMatch for _, m := range ignoredMatches { p := pkg.ByID(m.Package.ID, packages) if p == nil { return Document{}, fmt.Errorf("unable to find package in collection: %+v", p) } matchModel, err := newMatch(m.Match, *p, metadataProvider) if err != nil { return Document{}, err } ignoredMatch := IgnoredMatch{ Match: *matchModel, AppliedIgnoreRules: mapIgnoreRules(m.AppliedIgnoreRules), } ignoredMatchModels = append(ignoredMatchModels, ignoredMatch) } return Document{ Matches: findings, IgnoredMatches: ignoredMatchModels, AlertsByPackage: buildPackageAlerts(distroAlerts), Source: src, Distro: newDistribution(context, selectMostCommonDistro(packages)), Descriptor: descriptor{ Name: id.Name, Version: id.Version, Configuration: appConfig, DB: dbInfo, Timestamp: timestamp, }, }, nil } // createTimestamp creates a timestamp string for the document descriptor. func createTimestamp(outputTimestamp bool) (string, error) { if !outputTimestamp { return "", nil } timestamp, err := time.Now().Local().MarshalText() if err != nil { return "", err } return string(timestamp), nil } // distroString returns the distro string representation, or "unknown" if nil. func distroString(p pkg.Package) string { if p.Distro != nil { return p.Distro.String() } return "unknown" } // buildPackageAlerts creates PackageAlerts from distro tracking data. func buildPackageAlerts(data *DistroAlertData) []PackageAlerts { if data == nil { return nil } // map package ID to alerts for deduplication alertsByPkg := make(map[string]*PackageAlerts) // helper to add an alert for a package addAlert := func(p pkg.Package, alertType AlertType, message string, metadata any) { pkgID := string(p.ID) alert := Alert{ Type: alertType, Message: message, Metadata: metadata, } if existing, ok := alertsByPkg[pkgID]; ok { existing.Alerts = append(existing.Alerts, alert) } else { alertsByPkg[pkgID] = &PackageAlerts{ Package: newPackage(p), Alerts: []Alert{alert}, } } } // helper to extract distro metadata distroMetadata := func(p pkg.Package) DistroAlertMetadata { if p.Distro != nil { return DistroAlertMetadata{ Name: p.Distro.Name(), Version: p.Distro.VersionString(), } } return DistroAlertMetadata{Name: "unknown"} } // add alerts for EOL distro packages for _, p := range data.EOLDistroPackages { addAlert(p, AlertTypeDistroEOL, fmt.Sprintf("Package is from end-of-life distro: %s", distroString(p)), distroMetadata(p)) } // convert map to slice if len(alertsByPkg) == 0 { return nil } result := make([]PackageAlerts, 0, len(alertsByPkg)) for _, pa := range alertsByPkg { result = append(result, *pa) } slices.SortFunc(result, func(a, b PackageAlerts) int { return cmp.Compare(a.Package.ID, b.Package.ID) }) return result } // selectMostCommonDistro selects the most common distro from the provided packages. func selectMostCommonDistro(pkgs []pkg.Package) *distro.Distro { distros := make(map[string]*distro.Distro) count := make(map[string]int) var maxDistro *distro.Distro maxCount := 0 for _, p := range pkgs { if p.Distro != nil { s := p.Distro.String() count[s]++ if _, ok := distros[s]; !ok { distros[s] = p.Distro } if count[s] > maxCount { maxCount = count[s] maxDistro = p.Distro } } } return maxDistro } ================================================ FILE: grype/presenter/models/document_test.go ================================================ package models import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/anchore/clio" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" syftSource "github.com/anchore/syft/syft/source" ) func TestPackagesAreSorted(t *testing.T) { var pkg1 = pkg.Package{ ID: "package-1-id", Name: "package-1", Version: "1.1.1", Type: syftPkg.DebPkg, } var pkg2 = pkg.Package{ ID: "package-2-id", Name: "package-2", Version: "2.2.2", Type: syftPkg.DebPkg, } var match1 = match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-1999-0003"}, }, Package: pkg1, Details: match.Details{ { Type: match.ExactDirectMatch, }, }, } var match2 = match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-1999-0002"}, }, Package: pkg2, Details: match.Details{ { Type: match.ExactIndirectMatch, }, }, } var match3 = match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-1999-0001"}, }, Package: pkg1, Details: match.Details{ { Type: match.ExactIndirectMatch, }, }, } matches := match.NewMatches() matches.Add(match1, match2, match3) packages := []pkg.Package{pkg1, pkg2} ctx := pkg.Context{ Source: &syftSource.Description{ Metadata: syftSource.DirectoryMetadata{}, }, } doc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage, true, nil) if err != nil { t.Fatalf("unable to get document: %+v", err) } var actualPackages []string for _, m := range doc.Matches { actualPackages = append(actualPackages, m.Artifact.Name) } // sort packages first assert.Equal(t, []string{"package-1", "package-1", "package-2"}, actualPackages) var actualVulnerabilities []string for _, m := range doc.Matches { actualVulnerabilities = append(actualVulnerabilities, m.Vulnerability.ID) } // sort vulnerabilities second assert.Equal(t, []string{"CVE-1999-0001", "CVE-1999-0003", "CVE-1999-0002"}, actualVulnerabilities) } func TestFixSuggestedVersion(t *testing.T) { var pkg1 = pkg.Package{ ID: "package-1-id", Name: "package-1", Version: "1.1.1", Type: syftPkg.PythonPkg, } var match1 = match.Match{ Vulnerability: vulnerability.Vulnerability{ Fix: vulnerability.Fix{ Versions: []string{"1.0.0", "1.2.0", "1.1.2"}, }, Reference: vulnerability.Reference{ID: "CVE-1999-0003"}, }, Package: pkg1, Details: match.Details{ { Type: match.ExactDirectMatch, }, }, } matches := match.NewMatches() matches.Add(match1) packages := []pkg.Package{pkg1} ctx := pkg.Context{ Source: &syftSource.Description{ Metadata: syftSource.DirectoryMetadata{}, }, } doc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage, true, nil) if err != nil { t.Fatalf("unable to get document: %+v", err) } actualSuggestedFixedVersion := doc.Matches[0].MatchDetails[0].Fix.SuggestedVersion assert.Equal(t, "1.1.2", actualSuggestedFixedVersion) } func TestTimestampValidFormat(t *testing.T) { matches := match.NewMatches() ctx := pkg.Context{ Source: nil, } doc, err := NewDocument(clio.Identification{}, nil, ctx, matches, nil, nil, nil, nil, SortByPackage, true, nil) if err != nil { t.Fatalf("unable to get document: %+v", err) } assert.NotEmpty(t, doc.Descriptor.Timestamp) // Check format is RFC3339 compatible e.g. 2023-04-21T00:22:06.491137+01:00 _, timeErr := time.Parse(time.RFC3339, doc.Descriptor.Timestamp) if timeErr != nil { t.Fatalf("unable to parse time: %+v", timeErr) } } func TestConfigurableTimestamp(t *testing.T) { matches := match.NewMatches() ctx := pkg.Context{ Source: nil, Distro: nil, } doc, err := NewDocument(clio.Identification{}, nil, ctx, matches, nil, nil, nil, nil, SortByPackage, false, nil) if err != nil { t.Fatalf("unable to get document: %+v", err) } assert.Empty(t, doc.Descriptor.Timestamp) } func TestBuildPackageAlerts(t *testing.T) { ubuntu := &distro.Distro{Type: distro.Ubuntu, Version: "18.04"} pkg1 := pkg.Package{ ID: "pkg-1-id", Name: "openssl", Version: "1.1.1", Type: syftPkg.DebPkg, Distro: ubuntu, } pkg2 := pkg.Package{ ID: "pkg-2-id", Name: "curl", Version: "7.60.0", Type: syftPkg.DebPkg, Distro: ubuntu, } tests := []struct { name string data *DistroAlertData wantLen int wantAlerts map[string][]AlertType // package ID -> expected alert types }{ { name: "no distro alert data", data: nil, wantLen: 0, wantAlerts: map[string][]AlertType{}, }, { name: "EOL distro packages", data: &DistroAlertData{ EOLDistroPackages: []pkg.Package{pkg1, pkg2}, }, wantLen: 2, wantAlerts: map[string][]AlertType{ "pkg-1-id": {AlertTypeDistroEOL}, "pkg-2-id": {AlertTypeDistroEOL}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := buildPackageAlerts(tc.data) assert.Len(t, result, tc.wantLen) for _, pa := range result { expectedAlerts, ok := tc.wantAlerts[pa.Package.ID] assert.True(t, ok, "unexpected package in result: %s", pa.Package.ID) if tc.wantLen > 0 { assert.Len(t, pa.Alerts, len(expectedAlerts), "package should have expected number of alerts") // Check alert types match for i, expectedType := range expectedAlerts { assert.Equal(t, expectedType, pa.Alerts[i].Type) // Check message contains distro name assert.Contains(t, pa.Alerts[i].Message, "ubuntu") } } } }) } } ================================================ FILE: grype/presenter/models/ignore.go ================================================ package models import "github.com/anchore/grype/grype/match" type IgnoredMatch struct { Match AppliedIgnoreRules []IgnoreRule `json:"appliedIgnoreRules"` } type IgnoreRule struct { Vulnerability string `json:"vulnerability,omitempty"` Reason string `json:"reason,omitempty"` Namespace string `json:"namespace"` FixState string `json:"fix-state,omitempty"` Package *IgnoreRulePackage `json:"package,omitempty"` VexStatus string `json:"vex-status,omitempty"` VexJustification string `json:"vex-justification,omitempty"` MatchType string `json:"match-type,omitempty"` } type IgnoreRulePackage struct { Name string `json:"name,omitempty"` Version string `json:"version,omitempty"` Language string `json:"language"` Type string `json:"type,omitempty"` Location string `json:"location,omitempty"` UpstreamName string `json:"upstream-name,omitempty"` } func newIgnoreRule(r match.IgnoreRule) IgnoreRule { var ignoreRulePackage *IgnoreRulePackage // We'll only set the package part of the rule not to `nil` if there are any values to fill out. if p := r.Package; p.Name != "" || p.Version != "" || p.Type != "" || p.Location != "" { ignoreRulePackage = &IgnoreRulePackage{ Name: r.Package.Name, Version: r.Package.Version, Language: r.Package.Language, Type: r.Package.Type, Location: r.Package.Location, UpstreamName: r.Package.UpstreamName, } } return IgnoreRule{ Vulnerability: r.Vulnerability, Reason: r.Reason, Namespace: r.Namespace, FixState: r.FixState, Package: ignoreRulePackage, VexStatus: r.VexStatus, VexJustification: r.VexJustification, MatchType: string(r.MatchType), } } func mapIgnoreRules(rules []match.IgnoreRule) []IgnoreRule { var result []IgnoreRule for _, rule := range rules { result = append(result, newIgnoreRule(rule)) } return result } ================================================ FILE: grype/presenter/models/ignore_test.go ================================================ package models import ( "testing" "github.com/google/go-cmp/cmp" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/vulnerability" ) func TestNewIgnoreRule(t *testing.T) { cases := []struct { name string input match.IgnoreRule expected IgnoreRule }{ { name: "no values", input: match.IgnoreRule{}, expected: IgnoreRule{ Vulnerability: "", FixState: "", Package: nil, }, }, { name: "only vulnerability field", input: match.IgnoreRule{ Vulnerability: "CVE-2020-1234", }, expected: IgnoreRule{ Vulnerability: "CVE-2020-1234", }, }, { name: "only fix state field", input: match.IgnoreRule{ FixState: string(vulnerability.FixStateNotFixed), }, expected: IgnoreRule{ FixState: string(vulnerability.FixStateNotFixed), }, }, { name: "all package fields", input: match.IgnoreRule{ Package: match.IgnoreRulePackage{ Name: "libc", Version: "3.0.0", Type: "rpm", Location: "/some/location", }, }, expected: IgnoreRule{ Package: &IgnoreRulePackage{ Name: "libc", Version: "3.0.0", Type: "rpm", Location: "/some/location", }, }, }, { name: "only one package field", input: match.IgnoreRule{ Package: match.IgnoreRulePackage{ Type: "apk", }, }, expected: IgnoreRule{ Package: &IgnoreRulePackage{ Type: "apk", }, }, }, { name: "all fields", input: match.IgnoreRule{ Vulnerability: "CVE-2020-1234", FixState: string(vulnerability.FixStateNotFixed), Package: match.IgnoreRulePackage{ Name: "libc", Version: "3.0.0", Type: "rpm", Location: "/some/location", }, }, expected: IgnoreRule{ Vulnerability: "CVE-2020-1234", FixState: "not-fixed", Package: &IgnoreRulePackage{ Name: "libc", Version: "3.0.0", Type: "rpm", Location: "/some/location", }, }, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { actual := newIgnoreRule(testCase.input) if diff := cmp.Diff(testCase.expected, actual); diff != "" { t.Errorf("(-expected +actual):\n%s", diff) } }) } } ================================================ FILE: grype/presenter/models/match.go ================================================ package models import ( "fmt" "sort" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) // Match is a single item for the JSON array reported type Match struct { Vulnerability Vulnerability `json:"vulnerability"` RelatedVulnerabilities []VulnerabilityMetadata `json:"relatedVulnerabilities"` MatchDetails []MatchDetails `json:"matchDetails"` Artifact Package `json:"artifact"` } // MatchDetails contains all data that indicates how the result match was found type MatchDetails struct { Type string `json:"type"` Matcher string `json:"matcher"` SearchedBy interface{} `json:"searchedBy"` // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made. Found interface{} `json:"found"` // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within. Fix *FixDetails `json:"fix,omitempty"` } // FixDetails contains any data that is relevant to fixing the vulnerability specific to the package searched with type FixDetails struct { SuggestedVersion string `json:"suggestedVersion"` } //nolint:staticcheck // MetadataProvider is deprecated but still used internally func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.MetadataProvider) (*Match, error) { relatedVulnerabilities := make([]VulnerabilityMetadata, 0) for _, r := range m.Vulnerability.RelatedVulnerabilities { relatedMetadata, err := metadataProvider.VulnerabilityMetadata(r) //nolint:staticcheck // deprecated API still used internally if err != nil { return nil, fmt.Errorf("unable to fetch related vuln=%q metadata: %+v", r, err) } if relatedMetadata != nil { relatedVulnerabilities = append(relatedVulnerabilities, NewVulnerabilityMetadata(r.ID, r.Namespace, relatedMetadata)) } } // vulnerability.Vulnerability should always have vulnerability.Metadata populated, however, in the case of test mocks // and other edge cases, it may not be populated. In these cases, we should fetch the metadata from the provider. metadata := m.Vulnerability.Metadata if metadata == nil { var err error metadata, err = metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally if err != nil { return nil, fmt.Errorf("unable to fetch related vuln=%q metadata: %+v", m.Vulnerability.Reference, err) } } format := pkg.VersionFormat(p) details := make([]MatchDetails, len(m.Details)) for idx, d := range m.Details { details[idx] = MatchDetails{ Type: string(d.Type), Matcher: string(d.Matcher), SearchedBy: d.SearchedBy, Found: d.Found, Fix: getFix(m, p, format), } } return &Match{ Vulnerability: NewVulnerability(m.Vulnerability, metadata, format), Artifact: newPackage(p), RelatedVulnerabilities: relatedVulnerabilities, MatchDetails: details, }, nil } func getFix(m match.Match, p pkg.Package, format version.Format) *FixDetails { suggested := calculateSuggestedFixedVersion(p, m.Vulnerability.Fix.Versions, format) if suggested == "" { return nil } return &FixDetails{ SuggestedVersion: suggested, } } func calculateSuggestedFixedVersion(p pkg.Package, fixedVersions []string, format version.Format) string { if len(fixedVersions) == 0 { return "" } if len(fixedVersions) == 1 { return fixedVersions[0] } parseConstraint := func(constStr string) (version.Constraint, error) { constraint, err := version.GetConstraint(constStr, format) if err != nil { log.WithFields("package", p.Name).Trace("skipping sorting fixed versions") } return constraint, err } checkSatisfaction := func(constraint version.Constraint, v *version.Version) bool { satisfied, err := constraint.Satisfied(v) if err != nil { log.WithFields("package", p.Name).Trace("error while checking version satisfaction for sorting") } return satisfied && err == nil } sort.SliceStable(fixedVersions, func(i, j int) bool { v1 := version.New(fixedVersions[i], format) v2 := version.New(fixedVersions[j], format) err1 := v1.Validate() err2 := v2.Validate() if err1 != nil || err2 != nil { log.WithFields("package", p.Name).Trace("error while parsing version for sorting") return false } packageConstraint, err := parseConstraint(fmt.Sprintf("<=%s", p.Version)) if err != nil { return false } v1Satisfied := checkSatisfaction(packageConstraint, v1) v2Satisfied := checkSatisfaction(packageConstraint, v2) if v1Satisfied != v2Satisfied { return !v1Satisfied } internalConstraint, err := parseConstraint(fmt.Sprintf("<=%s", v1.Raw)) if err != nil { return false } return !checkSatisfaction(internalConstraint, v2) }) return fixedVersions[0] } ================================================ FILE: grype/presenter/models/metadata_mock.go ================================================ package models import ( "github.com/anchore/grype/grype/vulnerability" ) //nolint:staticcheck // MetadataProvider is deprecated but still used internally for testing var _ vulnerability.MetadataProvider = (*MetadataMock)(nil) // MetadataMock provides the behavior required for a vulnerability.Provider for the purpose of testing. type MetadataMock struct { store map[string]map[string]vulnerability.Metadata } type MockVendorMetadata struct { BaseSeverity string Status string } // NewMetadataMock returns a new instance of MetadataMock. func NewMetadataMock() *MetadataMock { return &MetadataMock{ store: map[string]map[string]vulnerability.Metadata{ "CVE-1999-0001": { "source-1": { Description: "1999-01 description", Severity: "Low", Cvss: []vulnerability.Cvss{ { Metrics: vulnerability.CvssMetrics{ BaseScore: 4, }, Vector: "another vector", Version: "3.0", }, }, }, }, "CVE-1999-0002": { "source-2": { Description: "1999-02 description", Severity: "Critical", Cvss: []vulnerability.Cvss{ { Metrics: vulnerability.CvssMetrics{ BaseScore: 1, ExploitabilityScore: ptr(2.0), ImpactScore: ptr(3.0), }, Vector: "vector", Version: "2.0", VendorMetadata: MockVendorMetadata{ BaseSeverity: "Low", Status: "verified", }, }, }, }, }, "CVE-1999-0003": { "source-1": { Description: "1999-03 description", Severity: "High", }, }, "CVE-1999-0004": { "source-2": { Description: "1999-04 description", Severity: "Critical", Cvss: []vulnerability.Cvss{ { Metrics: vulnerability.CvssMetrics{ BaseScore: 1, ExploitabilityScore: ptr(2.0), ImpactScore: ptr(3.0), }, Vector: "vector", Version: "2.0", VendorMetadata: MockVendorMetadata{ BaseSeverity: "Low", Status: "verified", }, }, }, }, }, }, } } func ptr[T any](t T) *T { return &t } // VulnerabilityMetadata returns vulnerability metadata for a given id and recordSource. func (m *MetadataMock) VulnerabilityMetadata(vuln vulnerability.Reference) (*vulnerability.Metadata, error) { value := m.store[vuln.ID][vuln.Namespace] value.ID = vuln.ID value.Namespace = vuln.Namespace return &value, nil } ================================================ FILE: grype/presenter/models/package.go ================================================ package models import ( "github.com/anchore/grype/grype/internal/packagemetadata" "github.com/anchore/grype/grype/pkg" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" ) // Package is meant to be only the fields that are needed when displaying a single pkg.Package object for the JSON presenter. type Package struct { ID string `json:"id"` Name string `json:"name"` Version string `json:"version"` Type syftPkg.Type `json:"type"` Locations file.Locations `json:"locations"` Language syftPkg.Language `json:"language"` Licenses []string `json:"licenses"` CPEs []string `json:"cpes"` PURL string `json:"purl"` Upstreams []UpstreamPackage `json:"upstreams"` MetadataType string `json:"metadataType,omitempty"` Metadata interface{} `json:"metadata,omitempty"` } type UpstreamPackage struct { Name string `json:"name"` Version string `json:"version,omitempty"` } func newPackage(p pkg.Package) Package { var cpes = make([]string, 0) for _, c := range p.CPEs { // use .String() to ensure proper escaping cpes = append(cpes, c.Attributes.String()) } licenses := p.Licenses if licenses == nil { licenses = make([]string, 0) } var upstreams = make([]UpstreamPackage, 0) for _, u := range p.Upstreams { upstreams = append(upstreams, UpstreamPackage{ Name: u.Name, Version: u.Version, }) } return Package{ ID: string(p.ID), Name: p.Name, Version: p.Version, Locations: p.Locations.ToSlice(), Licenses: licenses, Language: p.Language, Type: p.Type, CPEs: cpes, PURL: p.PURL, Upstreams: upstreams, MetadataType: packagemetadata.JSONName(p.Metadata), Metadata: p.Metadata, } } ================================================ FILE: grype/presenter/models/presenter_bundle.go ================================================ package models import ( "github.com/anchore/clio" "github.com/anchore/syft/syft/sbom" ) type PresenterConfig struct { ID clio.Identification Document Document SBOM *sbom.SBOM Pretty bool } ================================================ FILE: grype/presenter/models/sort.go ================================================ package models import ( "sort" "strings" "github.com/anchore/grype/internal/log" ) type SortStrategy string const ( SortByPackage SortStrategy = "package" SortBySeverity SortStrategy = "severity" SortByThreat SortStrategy = "epss" SortByRisk SortStrategy = "risk" SortByKEV SortStrategy = "kev" SortByVulnerability SortStrategy = "vulnerability" DefaultSortStrategy = SortByRisk ) func SortStrategies() []SortStrategy { return []SortStrategy{SortByPackage, SortBySeverity, SortByThreat, SortByRisk, SortByKEV, SortByVulnerability} } func (s SortStrategy) String() string { return string(s) } // compareFunc defines a comparison function between two Match values // Returns: // // -1: if a should come before b // 0: if a and b are equal for this comparison // 1: if a should come after b type compareFunc func(a, b Match) int // sortStrategyImpl defines a strategy for sorting with a slice of comparison functions type sortStrategyImpl []compareFunc // matchSortStrategy provides predefined sort strategies for Match var matchSortStrategy = map[SortStrategy]sortStrategyImpl{ SortByPackage: { comparePackageAttributes, compareVulnerabilityAttributes, }, SortByVulnerability: { compareVulnerabilityAttributes, comparePackageAttributes, }, SortBySeverity: { // severity and tangential attributes... compareBySeverity, compareByRisk, compareByEPSSPercentile, // followed by package attributes... comparePackageAttributes, // followed by the remaining vulnerability attributes... compareByVulnerabilityID, }, SortByThreat: { // epss and tangential attributes... compareByEPSSPercentile, compareByRisk, compareBySeverity, // followed by package attributes... comparePackageAttributes, // followed by the remaining vulnerability attributes... compareByVulnerabilityID, }, SortByRisk: { // risk and tangential attributes... compareByRisk, compareBySeverity, compareByEPSSPercentile, // followed by package attributes... comparePackageAttributes, // followed by the remaining vulnerability attributes... compareByVulnerabilityID, }, SortByKEV: { compareByKEV, // risk and tangential attributes... compareByRisk, compareBySeverity, compareByEPSSPercentile, // followed by package attributes... comparePackageAttributes, // followed by the remaining vulnerability attributes... compareByVulnerabilityID, }, } func compareVulnerabilityAttributes(a, b Match) int { return combine( compareByVulnerabilityID, compareByRisk, compareBySeverity, compareByEPSSPercentile, )(a, b) } func comparePackageAttributes(a, b Match) int { return combine( compareByPackageName, compareByPackageVersion, compareByPackageType, )(a, b) } func combine(impls ...compareFunc) compareFunc { return func(a, b Match) int { for _, impl := range impls { result := impl(a, b) if result != 0 { return result } } return 0 } } // SortMatches sorts matches based on a strategy name func SortMatches(matches []Match, strategyName SortStrategy) { sortWithStrategy(matches, getSortStrategy(strategyName)) } func getSortStrategy(strategyName SortStrategy) sortStrategyImpl { strategy, exists := matchSortStrategy[strategyName] if !exists { log.WithFields("strategy", strategyName).Debugf("unknown sort strategy, falling back to default of %q", DefaultSortStrategy) strategy = matchSortStrategy[DefaultSortStrategy] } return strategy } func sortWithStrategy(matches []Match, strategy sortStrategyImpl) { sort.Slice(matches, func(i, j int) bool { for _, compare := range strategy { result := compare(matches[i], matches[j]) if result != 0 { // we are implementing a "less" function, so we want to return true if the result is negative return result < 0 } } return false // all comparisons are equal }) } func compareByVulnerabilityID(a, b Match) int { aID := a.Vulnerability.ID bID := b.Vulnerability.ID switch { case aID < bID: return -1 case aID > bID: return 1 default: return 0 } } func compareBySeverity(a, b Match) int { aScore := severityPriority(a.Vulnerability.Severity) bScore := severityPriority(b.Vulnerability.Severity) switch { case aScore < bScore: // higher severity first return -1 case aScore > bScore: return 1 default: return 0 } } func compareByEPSSPercentile(a, b Match) int { aScore := epssPercentile(a.Vulnerability.EPSS) bScore := epssPercentile(b.Vulnerability.EPSS) switch { case aScore > bScore: // higher severity first return -1 case aScore < bScore: return 1 default: return 0 } } func compareByPackageName(a, b Match) int { aName := a.Artifact.Name bName := b.Artifact.Name switch { case aName < bName: return -1 case aName > bName: return 1 default: return 0 } } func compareByPackageVersion(a, b Match) int { aVersion := a.Artifact.Version bVersion := b.Artifact.Version switch { case aVersion < bVersion: return -1 case aVersion > bVersion: return 1 default: return 0 } } func compareByPackageType(a, b Match) int { aType := a.Artifact.Type bType := b.Artifact.Type switch { case aType < bType: return -1 case aType > bType: return 1 default: return 0 } } func compareByRisk(a, b Match) int { aRisk := a.Vulnerability.Risk bRisk := b.Vulnerability.Risk switch { case aRisk > bRisk: return -1 case aRisk < bRisk: return 1 default: return 0 } } func compareByKEV(a, b Match) int { aKEV := len(a.Vulnerability.KnownExploited) bKEV := len(b.Vulnerability.KnownExploited) switch { case aKEV > bKEV: return -1 case aKEV < bKEV: return 1 default: return 0 } } func epssPercentile(es []EPSS) float64 { switch len(es) { case 0: return 0.0 case 1: return es[0].Percentile } sort.Slice(es, func(i, j int) bool { return es[i].Percentile > es[j].Percentile }) return es[0].Percentile } // severityPriority maps severity strings to numeric priority for comparison (the lowest value is most severe) func severityPriority(severity string) int { switch strings.ToLower(severity) { case "critical": return 1 case "high": return 2 case "medium": return 3 case "low": return 4 case "negligible": return 5 default: return 100 // least severe } } ================================================ FILE: grype/presenter/models/sort_test.go ================================================ package models import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSortStrategies(t *testing.T) { strategies := SortStrategies() expected := []SortStrategy{ SortByPackage, SortBySeverity, SortByThreat, SortByRisk, SortByKEV, SortByVulnerability, } assert.Equal(t, expected, strategies) } func TestSortStrategyString(t *testing.T) { assert.Equal(t, "package", SortByPackage.String()) assert.Equal(t, "severity", SortBySeverity.String()) assert.Equal(t, "epss", SortByThreat.String()) assert.Equal(t, "risk", SortByRisk.String()) assert.Equal(t, "kev", SortByKEV.String()) assert.Equal(t, "vulnerability", SortByVulnerability.String()) } func TestGetSortStrategy(t *testing.T) { tests := []struct { name string strategyName SortStrategy expected bool }{ { name: "Valid strategy", strategyName: SortByPackage, expected: true, }, { name: "Invalid strategy", strategyName: "invalid", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { strategy := getSortStrategy(tt.strategyName) validStrategy, _ := matchSortStrategy[tt.strategyName] if tt.expected { require.NotNil(t, strategy) assert.Equal(t, validStrategy, strategy) } else { // Should fallback to default strategy assert.NotNil(t, strategy) assert.Equal(t, matchSortStrategy[DefaultSortStrategy], strategy) } }) } } func TestEPSSPercentile(t *testing.T) { tests := []struct { name string epss []EPSS expected float64 }{ { name: "Empty slice", epss: []EPSS{}, expected: 0.0, }, { name: "Single item", epss: []EPSS{ {Percentile: 0.75}, }, expected: 0.75, }, { name: "Multiple items, already sorted", epss: []EPSS{ {Percentile: 0.95}, {Percentile: 0.75}, {Percentile: 0.50}, }, expected: 0.95, }, { name: "Multiple items, unsorted", epss: []EPSS{ {Percentile: 0.50}, {Percentile: 0.95}, {Percentile: 0.75}, }, expected: 0.95, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := epssPercentile(tt.epss) assert.Equal(t, tt.expected, result) }) } } func TestSeverityPriority(t *testing.T) { tests := []struct { severity string expected int }{ {"critical", 1}, {"CRITICAL", 1}, {"high", 2}, {"HIGH", 2}, {"medium", 3}, {"MEDIUM", 3}, {"low", 4}, {"LOW", 4}, {"negligible", 5}, {"NEGLIGIBLE", 5}, {"unknown", 100}, {"", 100}, } for _, tt := range tests { t.Run(tt.severity, func(t *testing.T) { result := severityPriority(tt.severity) assert.Equal(t, tt.expected, result) }) } } func createTestMatches() []Match { return []Match{ { // match 0: medium severity, high risk, high EPSS, no KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-1111", Severity: "medium", EPSS: []EPSS{ {Percentile: 0.90}, }, KnownExploited: []KnownExploited{}, // empty KEV }, Risk: 75.0, }, Artifact: Package{ Name: "package-b", Version: "1.2.0", Type: "npm", }, }, { // match 1: critical severity, medium risk, medium EPSS, no KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-2222", Severity: "critical", EPSS: []EPSS{ {Percentile: 0.70}, }, KnownExploited: []KnownExploited{}, // empty KEV }, Risk: 50.0, }, Artifact: Package{ Name: "package-a", Version: "2.0.0", Type: "docker", }, }, { // match 2: high severity, low risk, low EPSS, has KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-3333", Severity: "high", EPSS: []EPSS{ {Percentile: 0.30}, }, KnownExploited: []KnownExploited{ {CVE: "CVE-2023-3333", KnownRansomwareCampaignUse: "No"}, }, // has KEV }, Risk: 25.0, }, Artifact: Package{ Name: "package-a", Version: "1.0.0", Type: "npm", }, }, { // match 3: low severity, very low risk, very low EPSS, no KEV Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-4444", Severity: "low", EPSS: []EPSS{ {Percentile: 0.10}, }, KnownExploited: []KnownExploited{}, // empty KEV }, Risk: 10.0, }, Artifact: Package{ Name: "package-c", Version: "3.1.0", Type: "gem", }, }, { // match 4: critical severity, very low risk, medium EPSS, has KEV with ransomware Vulnerability: Vulnerability{ VulnerabilityMetadata: VulnerabilityMetadata{ ID: "CVE-2023-5555", Severity: "critical", EPSS: []EPSS{ {Percentile: 0.50}, }, KnownExploited: []KnownExploited{ {CVE: "CVE-2023-5555", KnownRansomwareCampaignUse: "Known"}, {CVE: "CVE-2023-5555", KnownRansomwareCampaignUse: "Known", Product: "Different Product"}, }, // has multiple KEV entries with ransomware }, Risk: 5.0, }, Artifact: Package{ Name: "package-a", Version: "1.0.0", Type: "docker", }, }, } } func TestAllSortStrategies(t *testing.T) { matches := createTestMatches() tests := []struct { strategy SortStrategy expected []int // indexes into the original matches slice }{ { strategy: SortByPackage, expected: []int{4, 2, 1, 0, 3}, // sorted by package name, version, type }, { strategy: SortByVulnerability, expected: []int{0, 1, 2, 3, 4}, // sorted by vulnerability ID }, { strategy: SortBySeverity, expected: []int{1, 4, 2, 0, 3}, // sorted by severity: critical, critical, high, medium, low }, { strategy: SortByThreat, expected: []int{0, 1, 4, 2, 3}, // sorted by EPSS percentile: 0.90, 0.70, 0.50, 0.30, 0.10 }, { strategy: SortByRisk, expected: []int{0, 1, 2, 3, 4}, // sorted by risk: 75.0, 50.0, 25.0, 10.0, 5.0 }, { strategy: SortByKEV, expected: []int{4, 2, 0, 1, 3}, // sorted by KEV count: 2, 1, 0, 0, 0 (with ties broken by risk) }, } for _, tt := range tests { t.Run(string(tt.strategy), func(t *testing.T) { testMatches := deepCopyMatches(matches) SortMatches(testMatches, tt.strategy) expected := make([]Match, len(tt.expected)) for i, idx := range tt.expected { expected[i] = matches[idx] } if diff := cmp.Diff(expected, testMatches); diff != "" { t.Errorf("%s mismatch (-want +got):\n%s", tt.strategy, diff) } }) } } func TestIndividualCompareFunctions(t *testing.T) { ms := createTestMatches() m0 := ms[0] // medium severity, high risk, high EPSS, no KEV m1 := ms[1] // critical severity, medium risk, medium EPSS, no KEV m2 := ms[2] // high severity, low risk, low EPSS, has KEV m3 := ms[3] // low severity, very low risk, very low EPSS, no KEV m4 := ms[4] // critical severity, very low risk, medium EPSS, has KEV with ransomware tests := []struct { name string compareFunc compareFunc pairs []struct { a, b Match expected int } }{ { name: "compareByVulnerabilityID", compareFunc: compareByVulnerabilityID, pairs: []struct { a, b Match expected int }{ {m0, m1, -1}, // CVE-2023-1111 < CVE-2023-2222 {m1, m0, 1}, // CVE-2023-2222 > CVE-2023-1111 {m0, m0, 0}, // Same ID }, }, { name: "compareBySeverity", compareFunc: compareBySeverity, pairs: []struct { a, b Match expected int }{ {m0, m1, 1}, // medium > critical {m1, m0, -1}, // critical < medium {m1, m4, 0}, // both critical {m2, m3, -1}, // high < low }, }, { name: "compareByEPSSPercentile", compareFunc: compareByEPSSPercentile, pairs: []struct { a, b Match expected int }{ {m0, m1, -1}, // 0.90 > 0.70 {m1, m0, 1}, // 0.70 < 0.90 {m1, m4, -1}, // 0.70 > 0.50 {m4, m1, 1}, // 0.50 < 0.70 }, }, { name: "compareByPackageName", compareFunc: compareByPackageName, pairs: []struct { a, b Match expected int }{ {m0, m1, 1}, // package-b > package-a {m1, m0, -1}, // package-a < package-b {m1, m2, 0}, // both package-a }, }, { name: "compareByPackageVersion", compareFunc: compareByPackageVersion, pairs: []struct { a, b Match expected int }{ {m1, m2, 1}, // 2.0.0 > 1.0.0 {m2, m1, -1}, // 1.0.0 < 2.0.0 {m2, m4, 0}, // both 1.0.0 }, }, { name: "compareByPackageType", compareFunc: compareByPackageType, pairs: []struct { a, b Match expected int }{ {m0, m1, 1}, // npm > docker {m1, m0, -1}, // docker < npm {m0, m2, 0}, // both npm }, }, { name: "compareByRisk", compareFunc: compareByRisk, pairs: []struct { a, b Match expected int }{ {m0, m1, -1}, // 75.0 > 50.0 {m1, m0, 1}, // 50.0 < 75.0 {m3, m4, -1}, // 10.0 > 5.0 }, }, { name: "compareByKEV", compareFunc: compareByKEV, pairs: []struct { a, b Match expected int }{ {m0, m2, 1}, // 0 < 1 KEV entry {m2, m0, -1}, // 1 > 0 KEV entry {m2, m4, 1}, // 1 < 2 KEV entries {m4, m2, -1}, // 2 > 1 KEV entry {m0, m1, 0}, // both 0 KEV entries }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, pair := range tt.pairs { result := tt.compareFunc(pair.a, pair.b) assert.Equal(t, pair.expected, result, "comparing %v and %v", pair.a.Vulnerability.ID, pair.b.Vulnerability.ID) } }) } } func TestCombinedCompareFunctions(t *testing.T) { ms := createTestMatches() m0 := ms[0] // medium severity, high risk, high EPSS, no KEV, package-b m1 := ms[1] // critical severity, medium risk, medium EPSS, no KEV, package-a m2 := ms[2] // high severity, low risk, low EPSS, has KEV, package-a t.Run("compareVulnerabilityAttributes", func(t *testing.T) { result := compareVulnerabilityAttributes(m0, m1) assert.Equal(t, -1, result, "CVE-2023-1111 should come before CVE-2023-2222") result = compareVulnerabilityAttributes(m1, m0) assert.Equal(t, 1, result, "CVE-2023-2222 should come after CVE-2023-1111") }) t.Run("comparePackageAttributes", func(t *testing.T) { result := comparePackageAttributes(m0, m1) assert.Equal(t, 1, result, "package-b should come after package-a") result = comparePackageAttributes(m1, m2) assert.Equal(t, 1, result, "package-a 2.0.0 should come after package-a 1.0.0") result = comparePackageAttributes(m1, m1) assert.Equal(t, 0, result, "same package should be equal") }) t.Run("combine function", func(t *testing.T) { // create a combined function that first compares by severity, then by risk if severity is equal combined := combine(compareBySeverity, compareByRisk) result := combined(m0, m1) assert.Equal(t, 1, result, "medium should come after critical regardless of risk") // create two matches with the same severity but different risk m5 := m1 // critical severity, risk 50.0 m6 := m1 m6.Vulnerability.Risk = 60.0 // critical severity, risk 60.0 result = combined(m5, m6) assert.Equal(t, 1, result, "with equal severity, lower risk (50.0) should come after higher risk (60.0)") result = combined(m6, m5) assert.Equal(t, -1, result, "with equal severity, higher risk (60.0) should come before lower risk (50.0)") }) } func TestSortWithStrategy(t *testing.T) { matches := createTestMatches() // create a custom strategy that sorts only by vulnerability ID customStrategy := sortStrategyImpl{compareByVulnerabilityID} expected := []Match{ matches[0], // CVE-2023-1111 matches[1], // CVE-2023-2222 matches[2], // CVE-2023-3333 matches[3], // CVE-2023-4444 matches[4], // CVE-2023-5555 } testMatches := deepCopyMatches(matches) sortWithStrategy(testMatches, customStrategy) if diff := cmp.Diff(expected, testMatches); diff != "" { t.Errorf("sortWithStrategy mismatch (-want +got):\n%s", diff) } // create an empty strategy (should not change the order) emptyStrategy := sortStrategyImpl{} originalMatches := deepCopyMatches(matches) sortWithStrategy(originalMatches, emptyStrategy) if diff := cmp.Diff(matches, originalMatches); diff != "" { t.Errorf("Empty strategy should not change order (-original +after):\n%s", diff) } } func deepCopyMatches(matches []Match) []Match { result := make([]Match, len(matches)) copy(result, matches) return result } ================================================ FILE: grype/presenter/models/source.go ================================================ package models import ( "fmt" "github.com/anchore/grype/grype/pkg" syftSource "github.com/anchore/syft/syft/source" ) type source struct { Type string `json:"type"` Target interface{} `json:"target"` } // newSource creates a new source object to be represented into JSON. func newSource(src syftSource.Description) (source, error) { switch m := src.Metadata.(type) { case pkg.SBOMFileMetadata: return source{ Type: "sbom-file", Target: m.Path, }, nil case pkg.PURLLiteralMetadata: return source{ Type: "purl", Target: m.PURL, }, nil case pkg.CPELiteralMetadata: return source{ Type: "cpe", Target: m.CPE, }, nil case syftSource.ImageMetadata: // ensure that empty collections are not shown as null if m.RepoDigests == nil { m.RepoDigests = []string{} } if m.Tags == nil { m.Tags = []string{} } return source{ Type: "image", Target: m, }, nil case syftSource.OCIModelMetadata: // ensure that empty collections are not shown as null if m.RepoDigests == nil { m.RepoDigests = []string{} } if m.Tags == nil { m.Tags = []string{} } return source{ Type: "oci-model", Target: m, }, nil case syftSource.DirectoryMetadata: return source{ Type: "directory", Target: m.Path, }, nil case syftSource.FileMetadata: return source{ Type: "file", Target: m.Path, }, nil case syftSource.SnapMetadata: return source{ Type: "snap", Target: fmt.Sprintf("%s@%s", src.Name, src.Version), }, nil case nil: // we may be showing results from a input source that does not support source information return source{ Type: "unknown", Target: "unknown", }, nil default: return source{}, fmt.Errorf("unsupported source: %T", src.Metadata) } } ================================================ FILE: grype/presenter/models/source_test.go ================================================ package models import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/pkg" syftSource "github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/testutil" ) func TestNewSource(t *testing.T) { // there isn't a great way to programmatically find only source metadata types in the pkg package, so we'll add them here. grypeOnlySources := []any{ pkg.SBOMFileMetadata{}, pkg.PURLLiteralMetadata{}, pkg.CPELiteralMetadata{}, } tracker := testutil.NewSourceMetadataCompletionTester(t) tracker.Expect(grypeOnlySources...) testCases := []struct { name string metadata syftSource.Description expected source }{ { name: "image", metadata: syftSource.Description{ Metadata: syftSource.ImageMetadata{ UserInput: "abc", ID: "def", ManifestDigest: "abcdef", Size: 100, }, }, expected: source{ Type: "image", Target: syftSource.ImageMetadata{ UserInput: "abc", ID: "def", ManifestDigest: "abcdef", Size: 100, RepoDigests: []string{}, Tags: []string{}, }, }, }, { name: "directory", metadata: syftSource.Description{ Metadata: syftSource.DirectoryMetadata{ Path: "/foo/bar", }, }, expected: source{ Type: "directory", Target: "/foo/bar", }, }, { name: "file", metadata: syftSource.Description{ Metadata: syftSource.FileMetadata{ Path: "/foo/bar/test.zip", }, }, expected: source{ Type: "file", Target: "/foo/bar/test.zip", }, }, { name: "purl-file", metadata: syftSource.Description{ Metadata: pkg.SBOMFileMetadata{ Path: "/path/to/purls.txt", }, }, expected: source{ Type: "sbom-file", Target: "/path/to/purls.txt", }, }, { name: "purl-literal", metadata: syftSource.Description{ Metadata: pkg.PURLLiteralMetadata{ PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", }, }, expected: source{ Type: "purl", Target: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", }, }, { name: "cpe-literal", metadata: syftSource.Description{ Metadata: pkg.CPELiteralMetadata{ CPE: "cpe:/a:apache:log4j:2.14.1", }, }, expected: source{ Type: "cpe", Target: "cpe:/a:apache:log4j:2.14.1", }, }, { name: "snap metadata", metadata: syftSource.Description{ Name: "a-snap", Version: "10.2.3", Metadata: syftSource.SnapMetadata{}, }, expected: source{ Type: "snap", Target: "a-snap@10.2.3", }, }, { name: "oci model metadata", metadata: syftSource.Description{ Metadata: syftSource.OCIModelMetadata{ UserInput: "ai-model", ID: "ai-model-edf", ManifestDigest: "abcdef", Size: 100, }, }, expected: source{ Type: "oci-model", Target: syftSource.OCIModelMetadata{ UserInput: "ai-model", ID: "ai-model-edf", ManifestDigest: "abcdef", Size: 100, RepoDigests: []string{}, Tags: []string{}, }, }, }, { name: "nil metadata", metadata: syftSource.Description{ Metadata: nil, }, expected: source{ Type: "unknown", Target: "unknown", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual, err := newSource(testCase.metadata) require.NoError(t, err) assert.Equal(t, testCase.expected, actual) tracker.Tested(t, testCase.metadata.Metadata) }) } } ================================================ FILE: grype/presenter/models/vulnerability.go ================================================ package models import ( "sort" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) type Vulnerability struct { VulnerabilityMetadata Fix Fix `json:"fix"` Advisories []Advisory `json:"advisories"` Risk float64 `json:"risk"` } type Fix struct { Versions []string `json:"versions"` State string `json:"state"` Available []FixAvailable `json:"available,omitempty"` } type FixAvailable struct { Version string `json:"version"` Date string `json:"date"` Kind string `json:"kind,omitempty"` } type Advisory struct { ID string `json:"id"` Link string `json:"link"` } func NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability.Metadata, versionFormat version.Format) Vulnerability { if metadata == nil { return Vulnerability{ VulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata), } } advisories := make([]Advisory, len(vuln.Advisories)) for idx, advisory := range vuln.Advisories { advisories[idx] = Advisory{ ID: advisory.ID, Link: advisory.Link, } } fixedInVersions := vuln.Fix.Versions if fixedInVersions == nil { // always allocate collections fixedInVersions = make([]string, 0) } return Vulnerability{ VulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata), Fix: Fix{ Versions: sortVersions(fixedInVersions, versionFormat), State: string(vuln.Fix.State), Available: getFixAvailable(vuln.Fix.Available), }, Advisories: advisories, Risk: metadata.RiskScore(), } } func getFixAvailable(fixesAvailable []vulnerability.FixAvailable) []FixAvailable { if len(fixesAvailable) == 0 { return nil } var results []FixAvailable for _, fix := range fixesAvailable { if fix.Date.IsZero() { continue } f := FixAvailable{ Version: fix.Version, Date: fix.Date.Format("2006-01-02"), // just extract the Kind: fix.Kind, } results = append(results, f) } return results } func sortVersions(fixedVersions []string, format version.Format) []string { if len(fixedVersions) <= 1 { return fixedVersions } // first, create Version objects from strings (only once) versionObjs := make([]*version.Version, 0, len(fixedVersions)) var invalidVersions []string for _, vStr := range fixedVersions { v := version.New(vStr, format) err := v.Validate() if err != nil { log.WithFields("version", vStr, "error", err).Trace("error parsing version, skipping") invalidVersions = append(invalidVersions, vStr) continue } versionObjs = append(versionObjs, v) } // sort the Version objects sort.Slice(versionObjs, func(i, j int) bool { comparison, err := versionObjs[i].Compare(versionObjs[j]) if err != nil { log.WithFields("error", err).Trace("error comparing versions") return false } return comparison < 0 }) // convert back to strings var result []string for _, v := range versionObjs { result = append(result, v.Raw) } result = append(result, invalidVersions...) return result } ================================================ FILE: grype/presenter/models/vulnerability_metadata.go ================================================ package models import ( "time" "github.com/anchore/grype/grype/vulnerability" ) type VulnerabilityMetadata struct { ID string `json:"id"` DataSource string `json:"dataSource"` Namespace string `json:"namespace,omitempty"` Severity string `json:"severity,omitempty"` URLs []string `json:"urls"` Description string `json:"description,omitempty"` Cvss []Cvss `json:"cvss"` KnownExploited []KnownExploited `json:"knownExploited,omitempty"` EPSS []EPSS `json:"epss,omitempty"` CWEs []CWE `json:"cwes,omitempty"` } type KnownExploited struct { CVE string `json:"cve"` VendorProject string `json:"vendorProject,omitempty"` Product string `json:"product,omitempty"` DateAdded string `json:"dateAdded,omitempty"` RequiredAction string `json:"requiredAction,omitempty"` DueDate string `json:"dueDate,omitempty"` KnownRansomwareCampaignUse string `json:"knownRansomwareCampaignUse"` Notes string `json:"notes,omitempty"` URLs []string `json:"urls,omitempty"` CWEs []string `json:"cwes,omitempty"` } type EPSS struct { CVE string `json:"cve"` EPSS float64 `json:"epss"` Percentile float64 `json:"percentile"` Date string `json:"date"` } type CWE struct { Cve string `json:"cve"` CWE string `json:"cwe,omitempty"` Source string `json:"source,omitempty"` Type string `json:"type,omitempty"` } func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Metadata) VulnerabilityMetadata { if metadata == nil { return VulnerabilityMetadata{ ID: id, Namespace: namespace, } } urls := metadata.URLs if urls == nil { urls = make([]string, 0) } return VulnerabilityMetadata{ ID: id, DataSource: metadata.DataSource, Namespace: metadata.Namespace, Severity: metadata.Severity, URLs: urls, Description: metadata.Description, Cvss: toCVSS(metadata), KnownExploited: toKnownExploited(metadata.KnownExploited), EPSS: toEPSS(metadata.EPSS), CWEs: toCWE(metadata.CWEs), } } func toKnownExploited(knownExploited []vulnerability.KnownExploited) []KnownExploited { result := make([]KnownExploited, len(knownExploited)) for idx, ke := range knownExploited { result[idx] = KnownExploited{ CVE: ke.CVE, VendorProject: ke.VendorProject, Product: ke.Product, DateAdded: formatDate(ke.DateAdded), RequiredAction: ke.RequiredAction, DueDate: formatDate(ke.DueDate), KnownRansomwareCampaignUse: ke.KnownRansomwareCampaignUse, Notes: ke.Notes, URLs: ke.URLs, CWEs: ke.CWEs, } } return result } func formatDate(t *time.Time) string { if t == nil { return "" } return t.Format(time.DateOnly) } func toEPSS(epss []vulnerability.EPSS) []EPSS { result := make([]EPSS, len(epss)) for idx, e := range epss { result[idx] = EPSS{ CVE: e.CVE, EPSS: e.EPSS, Percentile: e.Percentile, Date: e.Date.Format(time.DateOnly), } } return result } func toCWE(cwes []vulnerability.CWE) []CWE { result := make([]CWE, len(cwes)) for idx, e := range cwes { result[idx] = CWE{ Cve: e.CVE, CWE: e.CWE, Source: e.Source, Type: e.Type, } } return result } ================================================ FILE: grype/presenter/models/vulnerability_test.go ================================================ package models import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" ) func Test_sortVersions(t *testing.T) { tests := []struct { name string versions []string expected []string }{ { name: "empty slice", versions: []string{}, expected: []string{}, }, { name: "single version", versions: []string{"1.0.0"}, expected: []string{"1.0.0"}, }, { name: "already sorted versions", versions: []string{"1.0.0", "1.1.0", "2.0.0"}, expected: []string{"1.0.0", "1.1.0", "2.0.0"}, }, { name: "unsorted versions", versions: []string{"2.0.0", "1.0.0", "1.1.0"}, expected: []string{"1.0.0", "1.1.0", "2.0.0"}, }, { name: "patch versions", versions: []string{"1.0.2", "1.0.1", "1.0.0"}, expected: []string{"1.0.0", "1.0.1", "1.0.2"}, }, { name: "versions with pre-release", versions: []string{"1.0.0", "1.0.0-alpha", "1.0.0-beta"}, expected: []string{"1.0.0-alpha", "1.0.0-beta", "1.0.0"}, }, { name: "mixed pre-release and regular", versions: []string{"2.0.0", "1.0.0-alpha", "1.0.0", "1.0.0-beta"}, expected: []string{"1.0.0-alpha", "1.0.0-beta", "1.0.0", "2.0.0"}, }, { name: "versions with build metadata", versions: []string{"1.0.0+build.2", "1.0.0+build.1", "1.0.0"}, expected: []string{"1.0.0+build.2", "1.0.0+build.1", "1.0.0"}, }, { name: "complex semantic versions", versions: []string{"1.0.0-alpha.1", "1.0.0-alpha", "1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1"}, expected: []string{"1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1"}, }, { name: "invalid versions are appended to the end (in the order they were found in)", versions: []string{"invalid", "2.0.0", "also-invalid", "1.0.0"}, expected: []string{"1.0.0", "2.0.0", "invalid", "also-invalid"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := sortVersions(tt.versions, version.SemanticFormat) if d := cmp.Diff(tt.expected, result); d != "" { t.Errorf("sortVersions() mismatch (-want +got):\n%s", d) } }) } } func Test_getFixAvailable(t *testing.T) { validDate1 := time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC) validDate2 := time.Date(2023, 3, 10, 0, 0, 0, 0, time.UTC) zeroDate := time.Time{} tests := []struct { name string input []vulnerability.FixAvailable expected []FixAvailable }{ { name: "empty input returns nil", input: []vulnerability.FixAvailable{}, expected: nil, }, { name: "single fix with valid date", input: []vulnerability.FixAvailable{ {Version: "1.2.3", Date: validDate1, Kind: "first-observed"}, }, expected: []FixAvailable{ {Version: "1.2.3", Date: "2023-01-15", Kind: "first-observed"}, }, }, { name: "multiple fixes with valid dates", input: []vulnerability.FixAvailable{ {Version: "1.2.3", Date: validDate1, Kind: "first-observed"}, {Version: "2.0.0", Date: validDate2, Kind: "first-observed"}, }, expected: []FixAvailable{ {Version: "1.2.3", Date: "2023-01-15", Kind: "first-observed"}, {Version: "2.0.0", Date: "2023-03-10", Kind: "first-observed"}, }, }, { name: "filters out fixes with zero dates", input: []vulnerability.FixAvailable{ {Version: "1.2.3", Date: validDate1, Kind: "first-observed"}, {Version: "1.2.4", Date: zeroDate, Kind: "first-observed"}, {Version: "2.0.0", Date: validDate2, Kind: "first-observed"}, }, expected: []FixAvailable{ {Version: "1.2.3", Date: "2023-01-15", Kind: "first-observed"}, {Version: "2.0.0", Date: "2023-03-10", Kind: "first-observed"}, }, }, { name: "all fixes with zero dates returns nil", input: []vulnerability.FixAvailable{ {Version: "1.2.3", Date: zeroDate, Kind: "first-observed"}, {Version: "1.2.4", Date: zeroDate, Kind: "first-observed"}, }, expected: nil, }, { name: "empty kind is preserved", input: []vulnerability.FixAvailable{ {Version: "1.0.0", Date: validDate1, Kind: ""}, }, expected: []FixAvailable{ {Version: "1.0.0", Date: "2023-01-15", Kind: ""}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getFixAvailable(tt.input) if d := cmp.Diff(tt.expected, result); d != "" { t.Errorf("getFixAvailable() mismatch (-want +got):\n%s", d) } }) } } ================================================ FILE: grype/presenter/presenter.go ================================================ package presenter import ( "github.com/wagoodman/go-presenter" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal/format" ) // GetPresenter retrieves a Presenter that matches a CLI option. // // Deprecated: this will be removed in v1.0 func GetPresenter(f string, templatePath string, showSuppressed bool, pb models.PresenterConfig) presenter.Presenter { return format.GetPresenter(format.Parse(f), format.PresentationConfig{ TemplateFilePath: templatePath, ShowSuppressed: showSuppressed, }, pb) } ================================================ FILE: grype/presenter/sarif/presenter.go ================================================ package sarif import ( "crypto/sha256" "fmt" "hash" "io" "path/filepath" "regexp" "strings" "github.com/owenrumney/go-sarif/sarif" "github.com/anchore/clio" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/source" ) // Presenter holds the data for generating a report and implements the presenter.Presenter interface type Presenter struct { id clio.Identification document models.Document src source.Description } // NewPresenter is a Presenter constructor func NewPresenter(pb models.PresenterConfig) *Presenter { return &Presenter{ id: pb.ID, document: pb.Document, src: pb.SBOM.Source, } } // Present creates a SARIF-based report func (p Presenter) Present(output io.Writer) error { doc, err := p.toSarifReport() if err != nil { return err } err = doc.PrettyWrite(output) return err } // toSarifReport outputs a sarif report object func (p Presenter) toSarifReport() (*sarif.Report, error) { doc, err := sarif.New(sarif.Version210) if err != nil { return nil, err } v := p.id.Version if v == "[not provided]" || v == "" { // Need a semver to pass the MS SARIF validator v = "0.0.0-dev" } doc.AddRun(&sarif.Run{ Tool: sarif.Tool{ Driver: &sarif.ToolComponent{ Name: p.id.Name, Version: sp(v), InformationURI: sp("https://github.com/anchore/grype"), Rules: p.sarifRules(), }, }, Results: p.sarifResults(), }) return doc, nil } // sarifRules generates the set of rules to include in this run func (p Presenter) sarifRules() (out []*sarif.ReportingDescriptor) { if len(p.document.Matches) > 0 { ruleIDs := map[string]bool{} for _, m := range p.document.Matches { ruleID := p.ruleID(m) if ruleIDs[ruleID] { // here, we're only outputting information about the vulnerabilities, not where we matched them continue } ruleIDs[ruleID] = true // Entirely possible to not have any links whatsoever link := m.Vulnerability.ID switch { case m.Vulnerability.DataSource != "": link = fmt.Sprintf("[%s](%s)", m.Vulnerability.ID, m.Vulnerability.DataSource) case len(m.Vulnerability.URLs) > 0: link = fmt.Sprintf("[%s](%s)", m.Vulnerability.ID, m.Vulnerability.URLs[0]) } descriptor := sarif.ReportingDescriptor{ ID: ruleID, Name: sp(ruleName(m)), HelpURI: sp("https://github.com/anchore/grype"), // Title of the SARIF report ShortDescription: &sarif.MultiformatMessageString{ Text: sp(shortDescription(m)), }, // Subtitle of the SARIF report FullDescription: &sarif.MultiformatMessageString{ Text: sp(subtitle(m)), }, Help: p.helpText(m, link), Properties: sarif.Properties{ // For GitHub reportingDescriptor object: // https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#reportingdescriptor-object "security-severity": securitySeverityValue(m), }, } if len(m.Artifact.PURL) != 0 { descriptor.Properties["purls"] = []string{m.Artifact.PURL} } out = append(out, &descriptor) } } return out } // ruleID creates a unique rule ID for a given match func (p Presenter) ruleID(m models.Match) string { // TODO if we support configuration, we may want to allow addition of another qualifier such that if multiple // vuln scans are run on multiple containers we can identify unique rules for each return fmt.Sprintf("%s-%s", m.Vulnerability.ID, m.Artifact.Name) } // helpText gets the help text for a rule, this is displayed in GitHub if you click on the title in a list of vulns func (p Presenter) helpText(m models.Match, link string) *sarif.MultiformatMessageString { // TODO we shouldn't necessarily be adding a location here, there may be multiple referencing the same vulnerability // we could instead add some list of all affected locations in the case there are a number found within an image, // for example but this might get more complicated if there are multiple vuln scans for a particular branch text := fmt.Sprintf("Vulnerability %s\nSeverity: %s\nPackage: %s\nVersion: %s\nFix Version: %s\nType: %s\nLocation: %s\nData Namespace: %s\nLink: %s", m.Vulnerability.ID, severityText(m), m.Artifact.Name, m.Artifact.Version, fixVersions(m), m.Artifact.Type, p.packagePath(m.Artifact), m.Vulnerability.Namespace, link, ) markdown := fmt.Sprintf( "**Vulnerability %s**\n"+ "| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n"+ "| --- | --- | --- | --- | --- | --- | --- | --- |\n"+ "| %s | %s | %s | %s | %s | %s | %s | %s |\n", m.Vulnerability.ID, severityText(m), m.Artifact.Name, m.Artifact.Version, fixVersions(m), m.Artifact.Type, p.packagePath(m.Artifact), m.Vulnerability.Namespace, link, ) return &sarif.MultiformatMessageString{ Text: &text, Markdown: &markdown, } } // packagePath attempts to get the relative path of the package to the "scan root" func (p Presenter) packagePath(a models.Package) string { if len(a.Locations) > 0 { return p.locationPath(a.Locations[0]) } return p.inputPath() } // inputPath returns a friendlier relative path or absolute path depending on the input, not prefixed by . or ./ func (p Presenter) inputPath() string { var inputPath string switch m := p.src.Metadata.(type) { case source.FileMetadata: inputPath = m.Path case source.DirectoryMetadata: inputPath = m.Path default: return "" } inputPath = strings.TrimPrefix(inputPath, "./") if inputPath == "." { return "" } return inputPath } // locationPath returns a path for the location, relative to the cwd func (p Presenter) locationPath(l file.Location) string { path := l.Path() in := p.inputPath() path = strings.TrimPrefix(path, "./") // trimmed off any ./ and accounted for dir:. for both path and input path _, ok := p.src.Metadata.(source.DirectoryMetadata) if ok { if filepath.IsAbs(path) || in == "" { return path } // return a path relative to the cwd, if it's not absolute return fmt.Sprintf("%s/%s", in, path) } return path } // locations the locations array is a single "physical" location with potentially multiple logical locations func (p Presenter) locations(m models.Match) []*sarif.Location { physicalLocation := p.packagePath(m.Artifact) var logicalLocations []*sarif.LogicalLocation switch metadata := p.src.Metadata.(type) { case source.ImageMetadata: img := metadata.UserInput locations := m.Artifact.Locations for _, l := range locations { trimmedPath := strings.TrimLeft(p.locationPath(l), "/") logicalLocations = append(logicalLocations, &sarif.LogicalLocation{ FullyQualifiedName: sp(fmt.Sprintf("%s@%s:/%s", img, l.FileSystemID, trimmedPath)), Name: sp(l.RealPath), }) } // GitHub requires paths for the location, but we really don't have any information about what // file(s) these originated from in the repository. e.g. which Dockerfile was used to build an image, // so we just use a short path-compatible image name here, not the entire user input as it may include // sha and/or tags which are likely to change between runs and aren't really necessary for a general // path to find file where the package originated physicalLocation = fmt.Sprintf("%s/%s", imageShortPathName(p.src), physicalLocation) case source.FileMetadata: locations := m.Artifact.Locations for _, l := range locations { logicalLocations = append(logicalLocations, &sarif.LogicalLocation{ FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", metadata.Path, p.locationPath(l))), Name: sp(l.RealPath), }) } case source.DirectoryMetadata: // DirectoryScheme is already handled, with input prepended if needed } return []*sarif.Location{ { PhysicalLocation: &sarif.PhysicalLocation{ ArtifactLocation: &sarif.ArtifactLocation{ URI: sp(physicalLocation), }, // TODO When grype starts reporting line numbers this will need to get updated Region: &sarif.Region{ StartLine: ip(1), StartColumn: ip(1), EndLine: ip(1), EndColumn: ip(1), }, }, LogicalLocations: logicalLocations, }, } } // severityText provides a textual representation of the severity level of the match func severityText(m models.Match) string { severity := vulnerability.ParseSeverity(m.Vulnerability.Severity) switch severity { case vulnerability.CriticalSeverity: return "critical" case vulnerability.HighSeverity: return "high" case vulnerability.MediumSeverity: return "medium" } return "low" } // cvssScore attempts to get the best CVSS score that our vulnerability data contains func cvssScore(m models.Match) float64 { all := []models.VulnerabilityMetadata{ m.Vulnerability.VulnerabilityMetadata, } all = append(all, m.RelatedVulnerabilities...) score := -1.0 // first check vendor-specific entries for _, m := range all { if m.Namespace == "nvd:cpe" { continue } for _, cvss := range m.Cvss { if cvss.Metrics.BaseScore > score { score = cvss.Metrics.BaseScore } } } if score > 0 { return score } // next, check nvd entries for _, m := range all { for _, cvss := range m.Cvss { if cvss.Metrics.BaseScore > score { score = cvss.Metrics.BaseScore } } } return score } // securitySeverityValue GitHub security-severity property uses a numeric severity value to determine whether things // are critical, high, etc.; this converts our vulnerability to a value within the ranges func securitySeverityValue(m models.Match) string { // this corresponds directly to the CVSS score, so we return this if we have it score := cvssScore(m) if score > 0 { return fmt.Sprintf("%.1f", score) } severity := vulnerability.ParseSeverity(m.Vulnerability.Severity) switch severity { case vulnerability.CriticalSeverity: return "9.0" case vulnerability.HighSeverity: return "7.0" case vulnerability.MediumSeverity: return "4.0" case vulnerability.LowSeverity: return "1.0" } return "0.0" } func levelValue(m models.Match) string { severity := vulnerability.ParseSeverity(m.Vulnerability.Severity) switch severity { case vulnerability.CriticalSeverity: return "error" case vulnerability.HighSeverity: return "error" case vulnerability.MediumSeverity: return "warning" } return "note" } // subtitle generates a subtitle for the given match func subtitle(m models.Match) string { subtitle := m.Vulnerability.Description if subtitle != "" { return subtitle } fixVersion := fixVersions(m) if fixVersion != "" { return fmt.Sprintf("Version %s is affected with an available fix in versions %s", m.Artifact.Version, fixVersion) } return fmt.Sprintf("Version %s is affected with no fixes reported yet.", m.Artifact.Version) } func fixVersions(m models.Match) string { if m.Vulnerability.Fix.State == vulnerability.FixStateFixed.String() && len(m.Vulnerability.Fix.Versions) > 0 { return strings.Join(m.Vulnerability.Fix.Versions, ",") } return "" } func shortDescription(m models.Match) string { return fmt.Sprintf("%s %s vulnerability for %s package", m.Vulnerability.ID, severityText(m), m.Artifact.Name) } func (p Presenter) sarifResults() []*sarif.Result { out := make([]*sarif.Result, 0) // make sure we have at least an empty array for _, m := range p.document.Matches { out = append(out, &sarif.Result{ RuleID: sp(p.ruleID(m)), Level: sp(levelValue(m)), Message: p.resultMessage(m), // According to the SARIF spec, it may be correct to use AnalysisTarget.URI to indicate a logical // file such as a "Dockerfile" but GitHub does not work well with this // GitHub requires partialFingerprints to upload to the API; these are automatically filled in // when using the CodeQL upload action. See: https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#providing-data-to-track-code-scanning-alerts-across-runs PartialFingerprints: p.partialFingerprints(m), Locations: p.locations(m), }) } return out } // ip returns an int pointer based on the provided value func ip(i int) *int { return &i } // sp returns a string pointer based on the provided value func sp(sarif string) *string { return &sarif } func (p Presenter) resultMessage(m models.Match) sarif.Message { path := p.packagePath(m.Artifact) src := p.inputPath() switch meta := p.src.Metadata.(type) { case source.ImageMetadata: src = fmt.Sprintf("in image %s at: %s", meta.UserInput, path) case source.FileMetadata, source.DirectoryMetadata: src = fmt.Sprintf("at: %s", path) case pkg.PURLLiteralMetadata: src = fmt.Sprintf("from purl literal %q", meta.PURL) case pkg.SBOMFileMetadata: src = fmt.Sprintf("from SBOM file %s", meta.Path) } message := fmt.Sprintf("A %s vulnerability in %s package: %s, version %s was found %s", severityText(m), m.Artifact.Type, m.Artifact.Name, m.Artifact.Version, src) return sarif.Message{ Text: &message, } } func (p Presenter) partialFingerprints(m models.Match) map[string]any { a := m.Artifact hasher := sha256.New() if meta, ok := p.src.Metadata.(source.ImageMetadata); ok { hashWrite(hasher, p.src.Name, meta.Architecture, meta.OS) } hashWrite(hasher, string(a.Type), a.Name, a.Version, p.packagePath(a)) return map[string]any{ // this is meant to include :, but there isn't line information here, so just include :1 "primaryLocationLineHash": fmt.Sprintf("%x:1", hasher.Sum([]byte{})), } } func hashWrite(hasher hash.Hash, values ...string) { for _, value := range values { _, _ = hasher.Write([]byte(value)) } } func ruleName(m models.Match) string { if len(m.MatchDetails) > 0 { d := m.MatchDetails[0] buf := strings.Builder{} for _, segment := range []string{d.Matcher, d.Type} { for _, part := range strings.Split(segment, "-") { buf.WriteString(strings.ToUpper(part[:1])) buf.WriteString(part[1:]) } } return buf.String() } return m.Vulnerability.ID } var nonPathChars = regexp.MustCompile("[^a-zA-Z0-9-_.]") // imageShortPathName returns path-compatible text describing the image. if the image name is the form // some/path/to/image, it will return the image portion of the name. func imageShortPathName(s source.Description) string { imageName := s.Name parts := strings.Split(imageName, "/") imageName = parts[len(parts)-1] imageName = nonPathChars.ReplaceAllString(imageName, "") return imageName } ================================================ FILE: grype/presenter/sarif/presenter_test.go ================================================ package sarif import ( "bytes" "flag" "os/exec" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal/testutils" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/directorysource" ) var updateSnapshot = flag.Bool("update", false, "update .golden files for sarif presenters") var validatorImage = "ghcr.io/anchore/sarif-validator:0.1.0@sha256:a0729d695e023740f5df6bcb50d134e88149bea59c63a896a204e88f62b564c6" func TestSarifPresenter(t *testing.T) { tests := []struct { name string scheme internal.SyftSource }{ { name: "directory", scheme: internal.DirectorySource, }, { name: "image", scheme: internal.ImageSource, }, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, tc.scheme) pres := NewPresenter(pb) err := pres.Present(&buffer) if err != nil { t.Fatal(err) } actual := buffer.Bytes() if *updateSnapshot { testutils.UpdateGoldenFileContents(t, actual) } var expected = testutils.GetGoldenFileContents(t) actual = internal.Redact(actual) expected = internal.Redact(expected) if d := cmp.Diff(string(expected), string(actual)); d != "" { t.Fatalf("(-want +got):\n%s", d) } }) } } func Test_SarifIsValid(t *testing.T) { if _, err := exec.LookPath("docker"); err != nil { t.Skip("docker not available") } tests := []struct { name string scheme internal.SyftSource }{ { name: "directory", scheme: internal.DirectorySource, }, { name: "image", scheme: internal.ImageSource, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buffer bytes.Buffer pb := internal.GeneratePresenterConfig(t, tc.scheme) pres := NewPresenter(pb) err := pres.Present(&buffer) require.NoError(t, err) cmd := exec.Command("docker", "run", "--rm", "-i", validatorImage) out := bytes.Buffer{} cmd.Stdout = &out cmd.Stderr = &out // pipe to the docker command cmd.Stdin = &buffer err = cmd.Run() if err != nil || cmd.ProcessState.ExitCode() != 0 { // valid t.Fatalf("error validating SARIF document: %s", out.String()) } }) } } func Test_locationPath(t *testing.T) { tests := []struct { name string metadata any real string virtual string expected string }{ { name: "dir:.", metadata: source.DirectoryMetadata{ Path: ".", }, real: "/home/usr/file", virtual: "file", expected: "file", }, { name: "dir:./", metadata: source.DirectoryMetadata{ Path: "./", }, real: "/home/usr/file", virtual: "file", expected: "file", }, { name: "dir:./someplace", metadata: source.DirectoryMetadata{ Path: "./someplace", }, real: "/home/usr/file", virtual: "file", expected: "someplace/file", }, { name: "dir:/someplace", metadata: source.DirectoryMetadata{ Path: "/someplace", }, real: "file", expected: "/someplace/file", }, { name: "dir:/someplace symlink", metadata: source.DirectoryMetadata{ Path: "/someplace", }, real: "/someplace/usr/file", virtual: "file", expected: "/someplace/file", }, { name: "dir:/someplace absolute", metadata: source.DirectoryMetadata{ Path: "/someplace", }, real: "/usr/file", expected: "/usr/file", }, { name: "file:/someplace/file", metadata: source.FileMetadata{ Path: "/someplace/file", }, real: "/usr/file", expected: "/usr/file", }, { name: "file:/someplace/file relative", metadata: source.FileMetadata{ Path: "/someplace/file", }, real: "file", expected: "file", }, { name: "image", metadata: source.ImageMetadata{ UserInput: "alpine:latest", }, real: "/etc/file", expected: "/etc/file", }, { name: "image symlink", metadata: source.ImageMetadata{ UserInput: "alpine:latest", }, real: "/etc/elsewhere/file", virtual: "/etc/file", expected: "/etc/file", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { pres := createDirPresenter(t) pres.src = source.Description{ Metadata: test.metadata, } path := pres.packagePath(models.Package{ Locations: file.NewLocationSet( file.NewVirtualLocation(test.real, test.virtual), ).ToSlice(), }) assert.Equal(t, test.expected, path) }) } } func createDirPresenter(t *testing.T) *Presenter { d := t.TempDir() newSrc, err := directorysource.NewFromPath(d) if err != nil { t.Fatal(err) } pb := internal.GeneratePresenterConfig(t, internal.DirectorySource) pb.SBOM.Source = newSrc.Describe() pres := NewPresenter(pb) return pres } func TestToSarifReport(t *testing.T) { tt := []struct { name string scheme internal.SyftSource locations map[string]string }{ { name: "directory", scheme: internal.DirectorySource, locations: map[string]string{ "CVE-1999-0001-package-1": "/some/path/somefile-1.txt", "CVE-1999-0002-package-2": "/some/path/somefile-2.txt", }, }, { name: "image", scheme: internal.ImageSource, locations: map[string]string{ "CVE-1999-0001-package-1": "user-input/somefile-1.txt", "CVE-1999-0002-package-2": "user-input/somefile-2.txt", }, }, } for _, tc := range tt { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() pb := internal.GeneratePresenterConfig(t, tc.scheme) pres := NewPresenter(pb) report, err := pres.toSarifReport() assert.NoError(t, err) assert.Len(t, report.Runs, 1) assert.NotEmpty(t, report.Runs) assert.NotEmpty(t, report.Runs[0].Results) assert.NotEmpty(t, report.Runs[0].Tool.Driver) assert.NotEmpty(t, report.Runs[0].Tool.Driver.Rules) // Sorted by vulnID, pkg name, ... run := report.Runs[0] assert.Len(t, run.Tool.Driver.Rules, 2) assert.Equal(t, "CVE-1999-0001-package-1", run.Tool.Driver.Rules[0].ID) assert.Equal(t, "CVE-1999-0002-package-2", run.Tool.Driver.Rules[1].ID) assert.Len(t, run.Results, 2) result := run.Results[0] assert.Equal(t, "CVE-1999-0001-package-1", *result.RuleID) assert.Equal(t, "note", *result.Level) assert.Len(t, result.Locations, 1) location := result.Locations[0] expectedLocation, ok := tc.locations[*result.RuleID] if !ok { t.Fatalf("no expected location for %s", *result.RuleID) } assert.Equal(t, expectedLocation, *location.PhysicalLocation.ArtifactLocation.URI) result = run.Results[1] assert.Equal(t, "CVE-1999-0002-package-2", *result.RuleID) assert.Equal(t, "error", *result.Level) assert.Len(t, result.Locations, 1) location = result.Locations[0] expectedLocation, ok = tc.locations[*result.RuleID] if !ok { t.Fatalf("no expected location for %s", *result.RuleID) } assert.Equal(t, expectedLocation, *location.PhysicalLocation.ArtifactLocation.URI) }) } } func Test_cvssScoreWithMissingMetadata(t *testing.T) { score := cvssScore(models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: "id", Namespace: "namespace", }, }, }) assert.Equal(t, float64(-1), score) } func Test_cvssScore(t *testing.T) { cvss := func(id string, namespace string, scores ...float64) models.VulnerabilityMetadata { values := make([]models.Cvss, 0, len(scores)) for _, score := range scores { values = append(values, models.Cvss{ Metrics: models.CvssMetrics{ BaseScore: score, }, }) } return models.VulnerabilityMetadata{ ID: id, Namespace: namespace, Cvss: values, } } nvd1 := cvss("1", "nvd:cpe", 1) notNvd1 := cvss("1", "not-nvd", 2) notNvd2 := cvss("2", "not-nvd", 3, 4) tests := []struct { name string match models.Match expected float64 }{ { name: "none", match: models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: "4", }, }, RelatedVulnerabilities: []models.VulnerabilityMetadata{ { ID: "7", Namespace: "nvd:cpe", // intentionally missing info... }, }, }, expected: -1, }, { name: "direct", match: models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: notNvd2, }, RelatedVulnerabilities: []models.VulnerabilityMetadata{ nvd1, }, }, expected: 4, }, { name: "related not nvd", match: models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: nvd1, }, RelatedVulnerabilities: []models.VulnerabilityMetadata{ nvd1, notNvd1, }, }, expected: 2, }, { name: "related nvd", match: models.Match{ Vulnerability: models.Vulnerability{ VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: "4", Namespace: "not-nvd", // intentionally missing info... }, }, RelatedVulnerabilities: []models.VulnerabilityMetadata{ nvd1, { ID: "7", Namespace: "not-nvd", // intentionally missing info... }, }, }, expected: 1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { score := cvssScore(test.match) assert.Equal(t, test.expected, score) }) } } func Test_imageShortPathName(t *testing.T) { tests := []struct { name string in string expected string }{ { name: "valid single name", in: "simple.-_name", expected: "simple.-_name", }, { name: "valid name in org", in: "some-org/some-image", expected: "some-image", }, { name: "name and org with many invalid chars", in: "some/*^&$#%$#@*(}{<><./,valid-()(#)@!(~@#$#%^&**[]{-chars", expected: "valid--chars", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := imageShortPathName( source.Description{ Name: test.in, Metadata: nil, }, ) assert.Equal(t, test.expected, got) }) } } ================================================ FILE: grype/presenter/sarif/testdata/image-simple/Dockerfile ================================================ # Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. FROM scratch ADD file-1.txt /somefile-1.txt ADD file-2.txt /somefile-2.txt # note: adding a directory will behave differently on docker engine v18 vs v19 ADD target / ================================================ FILE: grype/presenter/sarif/testdata/image-simple/file-1.txt ================================================ this file has contents ================================================ FILE: grype/presenter/sarif/testdata/image-simple/file-2.txt ================================================ file-2 contents! ================================================ FILE: grype/presenter/sarif/testdata/image-simple/target/really/nested/file-3.txt ================================================ another file! with lines... ================================================ FILE: grype/presenter/sarif/testdata/snapshot/TestSarifPresenter_directory.golden ================================================ { "version": "2.1.0", "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", "runs": [ { "tool": { "driver": { "name": "grype", "version": "0.0.0-dev", "informationUri": "https://github.com/anchore/grype", "rules": [ { "id": "CVE-1999-0001-package-1", "name": "DpkgMatcherExactDirectMatch", "shortDescription": { "text": "CVE-1999-0001 low vulnerability for package-1 package" }, "fullDescription": { "text": "Version 1.1.1 is affected with an available fix in versions 1.2.1,2.1.3,3.4.0" }, "helpUri": "https://github.com/anchore/grype", "help": { "text": "Vulnerability CVE-1999-0001\nSeverity: low\nPackage: package-1\nVersion: 1.1.1\nFix Version: 1.2.1,2.1.3,3.4.0\nType: rpm\nLocation: /some/path/somefile-1.txt\nData Namespace: \nLink: CVE-1999-0001", "markdown": "**Vulnerability CVE-1999-0001**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| low | package-1 | 1.1.1 | 1.2.1,2.1.3,3.4.0 | rpm | /some/path/somefile-1.txt | | CVE-1999-0001 |\n" }, "properties": { "security-severity": "8.2" } }, { "id": "CVE-1999-0002-package-2", "name": "DpkgMatcherExactIndirectMatch", "shortDescription": { "text": "CVE-1999-0002 critical vulnerability for package-2 package" }, "fullDescription": { "text": "Version 2.2.2 is affected with no fixes reported yet." }, "helpUri": "https://github.com/anchore/grype", "help": { "text": "Vulnerability CVE-1999-0002\nSeverity: critical\nPackage: package-2\nVersion: 2.2.2\nFix Version: \nType: deb\nLocation: /some/path/somefile-2.txt\nData Namespace: \nLink: CVE-1999-0002", "markdown": "**Vulnerability CVE-1999-0002**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| critical | package-2 | 2.2.2 | | deb | /some/path/somefile-2.txt | | CVE-1999-0002 |\n" }, "properties": { "purls": [ "pkg:deb/package-2@2.2.2" ], "security-severity": "8.5" } } ] } }, "results": [ { "ruleId": "CVE-1999-0001-package-1", "level": "note", "message": { "text": "A low vulnerability in rpm package: package-1, version 1.1.1 was found at: /some/path/somefile-1.txt" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "/some/path/somefile-1.txt" }, "region": { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 } } } ], "partialFingerprints": { "primaryLocationLineHash": "0eefd3962fe456b80e5ddad4ec777c7f75b3c0586db887eff1c98f376fff60ba:1" } }, { "ruleId": "CVE-1999-0002-package-2", "level": "error", "message": { "text": "A critical vulnerability in deb package: package-2, version 2.2.2 was found at: /some/path/somefile-2.txt" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "/some/path/somefile-2.txt" }, "region": { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 } } } ], "partialFingerprints": { "primaryLocationLineHash": "0d4ef10dce50e71641e9314195020cea18febe4c6a4a8145a485154383d4fe0b:1" } } ] } ] } ================================================ FILE: grype/presenter/sarif/testdata/snapshot/TestSarifPresenter_image.golden ================================================ { "version": "2.1.0", "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", "runs": [ { "tool": { "driver": { "name": "grype", "version": "0.0.0-dev", "informationUri": "https://github.com/anchore/grype", "rules": [ { "id": "CVE-1999-0001-package-1", "name": "DpkgMatcherExactDirectMatch", "shortDescription": { "text": "CVE-1999-0001 low vulnerability for package-1 package" }, "fullDescription": { "text": "Version 1.1.1 is affected with an available fix in versions 1.2.1,2.1.3,3.4.0" }, "helpUri": "https://github.com/anchore/grype", "help": { "text": "Vulnerability CVE-1999-0001\nSeverity: low\nPackage: package-1\nVersion: 1.1.1\nFix Version: 1.2.1,2.1.3,3.4.0\nType: rpm\nLocation: somefile-1.txt\nData Namespace: \nLink: CVE-1999-0001", "markdown": "**Vulnerability CVE-1999-0001**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| low | package-1 | 1.1.1 | 1.2.1,2.1.3,3.4.0 | rpm | somefile-1.txt | | CVE-1999-0001 |\n" }, "properties": { "security-severity": "8.2" } }, { "id": "CVE-1999-0002-package-2", "name": "DpkgMatcherExactIndirectMatch", "shortDescription": { "text": "CVE-1999-0002 critical vulnerability for package-2 package" }, "fullDescription": { "text": "Version 2.2.2 is affected with no fixes reported yet." }, "helpUri": "https://github.com/anchore/grype", "help": { "text": "Vulnerability CVE-1999-0002\nSeverity: critical\nPackage: package-2\nVersion: 2.2.2\nFix Version: \nType: deb\nLocation: somefile-2.txt\nData Namespace: \nLink: CVE-1999-0002", "markdown": "**Vulnerability CVE-1999-0002**\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| critical | package-2 | 2.2.2 | | deb | somefile-2.txt | | CVE-1999-0002 |\n" }, "properties": { "purls": [ "pkg:deb/package-2@2.2.2" ], "security-severity": "8.5" } } ] } }, "results": [ { "ruleId": "CVE-1999-0001-package-1", "level": "note", "message": { "text": "A low vulnerability in rpm package: package-1, version 1.1.1 was found in image user-input at: somefile-1.txt" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "user-input/somefile-1.txt" }, "region": { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 } }, "logicalLocations": [ { "name": "/foo/bar/somefile-1.txt", "fullyQualifiedName": "user-input@:/somefile-1.txt" } ] } ], "partialFingerprints": { "primaryLocationLineHash": "efe125c0a2b4bdafe476b69ba51a49734780c62b93803950319056acebe4323f:1" } }, { "ruleId": "CVE-1999-0002-package-2", "level": "error", "message": { "text": "A critical vulnerability in deb package: package-2, version 2.2.2 was found in image user-input at: somefile-2.txt" }, "locations": [ { "physicalLocation": { "artifactLocation": { "uri": "user-input/somefile-2.txt" }, "region": { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 } }, "logicalLocations": [ { "name": "/foo/bar/somefile-2.txt", "fullyQualifiedName": "user-input@:/somefile-2.txt" } ] } ], "partialFingerprints": { "primaryLocationLineHash": "bafe9890c7cda00bf4d1b1a57d1d20b08e27162e718235a3d38a9a8d2f449ed1:1" } } ] } ] } ================================================ FILE: grype/presenter/table/__snapshots__/presenter_test.snap ================================================ [TestTablePresenter/no_color - 1] NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 3.0% (42nd) 1.7 package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev) --- [TestTablePresenter/with_color - 1] NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK package-1 1.1.1 1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 3.0% (42nd) 1.7 package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3  KEV   --- [TestEmptyTablePresenter - 1] No vulnerabilities found --- [TestHidesIgnoredMatches - 1] NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 3.0% (42nd) 1.7 package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev) --- [TestDisplaysIgnoredMatches - 1] NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 3.0% (42nd) 1.7 package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev) package-2 2.2.2 deb CVE-1999-0001 Low 3.0% (42nd) 1.7 (suppressed) package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev, suppressed) package-2 2.2.2 deb CVE-1999-0004 High 3.0% (75th) 2.2 (suppressed by VEX) --- [TestDisplaysDistro - 1] NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 3.0% (42nd) 1.7 (ubuntu:2.5) package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev, ubuntu:3.5) --- [TestDisplaysIgnoredMatchesAndDistro - 1] NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK package-1 1.1.1 *1.2.1, 2.1.3, 3.4.0 rpm CVE-1999-0001 Low 3.0% (42nd) 1.7 (ubuntu:2.5) package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev, ubuntu:3.5) package-2 2.2.2 deb CVE-1999-0001 Low 3.0% (42nd) 1.7 (ubuntu:2.5, suppressed) package-2 2.2.2 deb CVE-1999-0002 Critical 8.0% (53rd) 96.3 (kev, ubuntu:3.5, suppressed) package-2 2.2.2 deb CVE-1999-0004 High 3.0% (75th) 2.2 (suppressed by VEX) --- ================================================ FILE: grype/presenter/table/presenter.go ================================================ package table import ( "fmt" "io" "strings" "github.com/charmbracelet/lipgloss" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" "github.com/scylladb/go-set/strset" "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" ) const ( appendSuppressed = "suppressed" appendSuppressedVEX = "suppressed by VEX" ) // Presenter is a generic struct for holding fields needed for reporting type Presenter struct { document models.Document showSuppressed bool withColor bool recommendedFixStyle lipgloss.Style kevStyle lipgloss.Style criticalStyle lipgloss.Style highStyle lipgloss.Style mediumStyle lipgloss.Style lowStyle lipgloss.Style negligibleStyle lipgloss.Style auxiliaryStyle lipgloss.Style unknownStyle lipgloss.Style } type rows []row type row struct { Name string Version string Fix string PackageType string VulnerabilityID string Severity string EPSS epss Risk string Annotation string } type epss struct { Score float64 Percentile float64 } func (e epss) String() string { if e.Percentile == 0 { return "N/A" } probability := e.Score * 100 percentile := e.Percentile * 100 if probability < 0.1 { return fmt.Sprintf("< 0.1%% (%s)", formatPercentileWithSuffix(percentile)) } return fmt.Sprintf("%.1f%% (%s)", probability, formatPercentileWithSuffix(percentile)) } func formatPercentileWithSuffix(percentile float64) string { p := int(percentile) // Handle special cases for 11th, 12th, 13th if p%100 >= 11 && p%100 <= 13 { return fmt.Sprintf("%dth", p) } // Handle other cases switch p % 10 { case 1: return fmt.Sprintf("%dst", p) case 2: return fmt.Sprintf("%dnd", p) case 3: return fmt.Sprintf("%drd", p) default: return fmt.Sprintf("%dth", p) } } // NewPresenter is a *Presenter constructor func NewPresenter(pb models.PresenterConfig, showSuppressed bool) *Presenter { withColor := supportsColor() fixStyle := lipgloss.NewStyle().Border(lipgloss.Border{Left: "*"}, false, false, false, true) if withColor { fixStyle = lipgloss.NewStyle() } return &Presenter{ document: pb.Document, showSuppressed: showSuppressed, withColor: withColor, recommendedFixStyle: fixStyle, negligibleStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), // dark gray lowStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("36")), // cyan/teal mediumStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("178")), // gold/amber highStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("203")), // salmon/light red criticalStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("198")).Bold(true), // bright pink kevStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("198")).Reverse(true).Bold(true), // white on bright pink //kevStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("198")), // bright pink auxiliaryStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), // dark gray unknownStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), // light blue } } // Present creates a JSON-based reporting func (p *Presenter) Present(output io.Writer) error { rs := p.getRows(p.document, p.showSuppressed) if len(rs) == 0 { _, err := io.WriteString(output, "No vulnerabilities found\n") return err } table := newTable(output, []string{"Name", "Installed", "Fixed In", "Type", "Vulnerability", "Severity", "EPSS", "Risk"}) if err := table.Bulk(rs.Render()); err != nil { return fmt.Errorf("failed to add table rows: %w", err) } return table.Render() } func newTable(output io.Writer, columns []string) *tablewriter.Table { return tablewriter.NewTable(output, tablewriter.WithHeader(columns), tablewriter.WithHeaderAlignment(tw.AlignLeft), tablewriter.WithHeaderAutoWrap(tw.WrapNone), tablewriter.WithRowAutoWrap(tw.WrapNone), tablewriter.WithAutoHide(tw.On), tablewriter.WithRenderer(renderer.NewBlueprint()), tablewriter.WithBehavior( tw.Behavior{ TrimSpace: tw.On, AutoHide: tw.On, }, ), tablewriter.WithPadding( tw.Padding{ Right: " ", }, ), tablewriter.WithRendition( tw.Rendition{ Symbols: tw.NewSymbols(tw.StyleNone), Settings: tw.Settings{ Lines: tw.Lines{ ShowTop: tw.Off, ShowBottom: tw.Off, ShowHeaderLine: tw.Off, ShowFooterLine: tw.Off, }, }, }, ), ) } func (p *Presenter) getRows(doc models.Document, showSuppressed bool) rows { var rs rows multipleDistros := false existingDistro := "" for _, m := range doc.Matches { if _, err := distro.FromString(m.Vulnerability.Namespace); err == nil { if existingDistro == "" { existingDistro = m.Vulnerability.Namespace } else if existingDistro != m.Vulnerability.Namespace { multipleDistros = true break } } } // generate rows for matching vulnerabilities for _, m := range doc.Matches { rs = append(rs, p.newRow(m, "", multipleDistros)) } // generate rows for suppressed vulnerabilities if showSuppressed { for _, m := range doc.IgnoredMatches { msg := appendSuppressed if m.AppliedIgnoreRules != nil { for i := range m.AppliedIgnoreRules { if m.AppliedIgnoreRules[i].Namespace == "vex" { msg = appendSuppressedVEX } } } rs = append(rs, p.newRow(m.Match, msg, multipleDistros)) } } return rs } func supportsColor() bool { return lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Render("") != "" } func (p *Presenter) newRow(m models.Match, extraAnnotation string, showDistro bool) row { var annotations []string if showDistro { if d, err := distro.FromString(m.Vulnerability.Namespace); err == nil { annotations = append(annotations, p.auxiliaryStyle.Render(fmt.Sprintf("%s:%s", d.DistroType(), d.Version()))) } } if extraAnnotation != "" { annotations = append(annotations, p.auxiliaryStyle.Render(extraAnnotation)) } var kev, annotation string if len(m.Vulnerability.KnownExploited) > 0 { if p.withColor { kev = p.kevStyle.Render(" KEV ") // ⚡❋◆◉፨⿻⨳✖• (requires non-standard fonts:  ) } else { annotations = append([]string{"kev"}, annotations...) } } if len(annotations) > 0 { annotation = p.auxiliaryStyle.Render("(") + strings.Join(annotations, p.auxiliaryStyle.Render(", ")) + p.auxiliaryStyle.Render(")") } if kev != "" { annotation = kev + " " + annotation } return row{ Name: m.Artifact.Name, Version: m.Artifact.Version, Fix: p.formatFix(m), PackageType: string(m.Artifact.Type), VulnerabilityID: m.Vulnerability.ID, Severity: p.formatSeverity(m.Vulnerability.Severity), EPSS: newEPSS(m.Vulnerability.EPSS), Risk: p.formatRisk(m.Vulnerability.Risk), Annotation: annotation, } } func newEPSS(es []models.EPSS) epss { if len(es) == 0 { return epss{} } return epss{ Score: es[0].EPSS, Percentile: es[0].Percentile, } } func (p *Presenter) formatSeverity(severity string) string { var severityStyle *lipgloss.Style switch strings.ToLower(severity) { case "critical": severityStyle = &p.criticalStyle case "high": severityStyle = &p.highStyle case "medium": severityStyle = &p.mediumStyle case "low": severityStyle = &p.lowStyle case "negligible": severityStyle = &p.negligibleStyle } if severityStyle == nil { severityStyle = &p.unknownStyle } return severityStyle.Render(severity) } func (p *Presenter) formatRisk(risk float64) string { // TODO: add color to risk? switch { case risk == 0: return " N/A" case risk < 0.1: return "< 0.1" } return fmt.Sprintf("%5.1f", risk) } func (p *Presenter) formatFix(m models.Match) string { // adjust the model fix state values for better presentation switch m.Vulnerability.Fix.State { case vulnerability.FixStateWontFix.String(): return "(won't fix)" case vulnerability.FixStateUnknown.String(): return "" } // do our best to summarize the fixed versions, de-epmhasize non-recommended versions // also, since there is not a lot of screen real estate, we will truncate the list of fixed versions // to ~30 characters (or so) to avoid wrapping. return p.applyTruncation( p.formatVersionsToDisplay( m, getRecommendedVersions(m), ), m.Vulnerability.Fix.Versions, ) } func getRecommendedVersions(m models.Match) *strset.Set { recommended := strset.New() for _, d := range m.MatchDetails { if d.Fix == nil { continue } if d.Fix.SuggestedVersion != "" { recommended.Add(d.Fix.SuggestedVersion) } } return recommended } const maxVersionFieldLength = 30 func (p *Presenter) formatVersionsToDisplay(m models.Match, recommendedVersions *strset.Set) []string { hasMultipleVersions := len(m.Vulnerability.Fix.Versions) > 1 shouldHighlightRecommended := hasMultipleVersions && recommendedVersions.Size() > 0 var currentCharacterCount int added := strset.New() var vers []string for _, v := range m.Vulnerability.Fix.Versions { if added.Has(v) { continue // skip duplicates } if shouldHighlightRecommended { if recommendedVersions.Has(v) { // recommended versions always get added added.Add(v) currentCharacterCount += len(v) vers = append(vers, p.recommendedFixStyle.Render(v)) continue } // skip not-necessarily-recommended versions if we're running out of space if currentCharacterCount+len(v) > maxVersionFieldLength { continue } // add not-necessarily-recommended versions with auxiliary styling currentCharacterCount += len(v) added.Add(v) vers = append(vers, p.auxiliaryStyle.Render(v)) } else { // when not prioritizing, add all versions added.Add(v) vers = append(vers, v) } } return vers } func (p *Presenter) applyTruncation(formattedVersions []string, allVersions []string) string { finalVersions := strings.Join(formattedVersions, p.auxiliaryStyle.Render(", ")) var characterCount int for _, v := range allVersions { characterCount += len(v) } if characterCount > maxVersionFieldLength && len(allVersions) > 1 { finalVersions += p.auxiliaryStyle.Render(", ...") } return finalVersions } func (r row) Columns() []string { if r.Annotation != "" { return []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity, r.EPSS.String(), r.Risk, r.Annotation} } return []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity, r.EPSS.String(), r.Risk} } func (r row) String() string { return strings.Join(r.Columns(), "|") } func (rs rows) Render() [][]string { deduped := rs.Deduplicate() out := make([][]string, len(deduped)) for idx, r := range deduped { out[idx] = r.Columns() } return out } func (rs rows) Deduplicate() []row { // deduplicate seen := map[string]row{} var deduped rows for _, v := range rs { key := v.String() if _, ok := seen[key]; ok { // dup! continue } seen[key] = v deduped = append(deduped, v) } // render final columns return deduped } ================================================ FILE: grype/presenter/table/presenter_test.go ================================================ package table import ( "bytes" "testing" "github.com/charmbracelet/lipgloss" "github.com/gkampitakis/go-snaps/snaps" "github.com/google/go-cmp/cmp" "github.com/muesli/termenv" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/clio" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) func TestCreateRow(t *testing.T) { pkg1 := models.Package{ ID: "package-1-id", Name: "package-1", Version: "2.0.0", Type: syftPkg.DebPkg, } match1 := models.Match{ Vulnerability: models.Vulnerability{ Fix: models.Fix{ Versions: []string{"1.0.2", "2.0.1", "3.0.4"}, State: vulnerability.FixStateFixed.String(), }, Risk: 87.2, VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: "CVE-1999-0001", Namespace: "source-1", Description: "1999-01 description", Severity: "Medium", Cvss: []models.Cvss{ { Metrics: models.CvssMetrics{ BaseScore: 7, }, Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H", Version: "3.1", }, }, EPSS: []models.EPSS{ { CVE: "CVE-1999-0001", EPSS: 0.3, Percentile: 0.5, }, }, }, }, Artifact: pkg1, MatchDetails: []models.MatchDetails{ { Type: match.ExactDirectMatch.String(), Matcher: match.DpkgMatcher.String(), Fix: &models.FixDetails{ SuggestedVersion: "2.0.1", }, }, }, } matchWithKev := match1 matchWithKev.Vulnerability.KnownExploited = append(matchWithKev.Vulnerability.KnownExploited, models.KnownExploited{ CVE: "CVE-1999-0001", KnownRansomwareCampaignUse: "Known", }) cases := []struct { name string match models.Match extraAnnotation string expectedRow []string }{ { name: "create row for vulnerability", match: match1, extraAnnotation: "", expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Medium", "30.0% (50th)", " 87.2"}, }, { name: "create row for suppressed vulnerability", match: match1, extraAnnotation: appendSuppressed, expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Medium", "30.0% (50th)", " 87.2", "(suppressed)"}, }, { name: "create row for suppressed vulnerability + Kev", match: matchWithKev, extraAnnotation: appendSuppressed, expectedRow: []string{match1.Artifact.Name, match1.Artifact.Version, "1.0.2, *2.0.1, 3.0.4", string(match1.Artifact.Type), match1.Vulnerability.ID, "Medium", "30.0% (50th)", " 87.2", "(kev, suppressed)"}, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { p := NewPresenter(models.PresenterConfig{}, false) row := p.newRow(testCase.match, testCase.extraAnnotation, false) cols := rows{row}.Render()[0] assert.Equal(t, testCase.expectedRow, cols) }) } } func TestTablePresenter(t *testing.T) { pb := internal.GeneratePresenterConfig(t, internal.ImageSource) t.Run("no color", func(t *testing.T) { var buffer bytes.Buffer lipgloss.SetColorProfile(termenv.Ascii) pres := NewPresenter(pb, false) err := pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) }) t.Run("with color", func(t *testing.T) { var buffer bytes.Buffer lipgloss.SetColorProfile(termenv.TrueColor) t.Cleanup(func() { // don't affect other tests lipgloss.SetColorProfile(termenv.Ascii) }) pres := NewPresenter(pb, false) err := pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) }) } func TestEmptyTablePresenter(t *testing.T) { // Expected to have no output var buffer bytes.Buffer doc, err := models.NewDocument(clio.Identification{}, nil, pkg.Context{}, match.NewMatches(), nil, nil, nil, nil, models.SortByPackage, true, nil) require.NoError(t, err) pb := models.PresenterConfig{ Document: doc, } pres := NewPresenter(pb, false) // run presenter err = pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) } func TestHidesIgnoredMatches(t *testing.T) { var buffer bytes.Buffer pb := models.PresenterConfig{ Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource), } pres := NewPresenter(pb, false) err := pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) } func TestDisplaysIgnoredMatches(t *testing.T) { var buffer bytes.Buffer pb := models.PresenterConfig{ Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource), } pres := NewPresenter(pb, true) err := pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) } func TestDisplaysDistro(t *testing.T) { var buffer bytes.Buffer pb := models.PresenterConfig{ Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource), } pb.Document.Matches[0].Vulnerability.Namespace = "ubuntu:distro:ubuntu:2.5" pb.Document.Matches[1].Vulnerability.Namespace = "ubuntu:distro:ubuntu:3.5" pres := NewPresenter(pb, false) err := pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) } func TestDisplaysIgnoredMatchesAndDistro(t *testing.T) { var buffer bytes.Buffer pb := models.PresenterConfig{ Document: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource), } pb.Document.Matches[0].Vulnerability.Namespace = "ubuntu:distro:ubuntu:2.5" pb.Document.Matches[1].Vulnerability.Namespace = "ubuntu:distro:ubuntu:3.5" pb.Document.IgnoredMatches[0].Vulnerability.Namespace = "ubuntu:distro:ubuntu:2.5" pb.Document.IgnoredMatches[1].Vulnerability.Namespace = "ubuntu:distro:ubuntu:3.5" pres := NewPresenter(pb, true) err := pres.Present(&buffer) require.NoError(t, err) actual := buffer.String() snaps.MatchSnapshot(t, actual) } func TestRowsRender(t *testing.T) { t.Run("empty rows returns empty slice", func(t *testing.T) { var rs rows result := rs.Render() assert.Empty(t, result) }) t.Run("deduplicates identical rows", func(t *testing.T) { rs := rows{ mustRow(t, "pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", vulnerability.FixStateFixed), mustRow(t, "pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", vulnerability.FixStateFixed), } result := rs.Render() expected := [][]string{ {"pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", "3.0% (75th)", " N/A"}, } if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("Render() mismatch (-want +got):\n%s", diff) } }) t.Run("renders won't fix and empty fix versions correctly", func(t *testing.T) { // Create rows with different fix states row1 := mustRow(t, "pkgA", "1.0.0", "", "os", "CVE-2023-1234", "critical", vulnerability.FixStateUnknown) row2 := mustRow(t, "pkgB", "2.0.0", "", "os", "CVE-2023-5678", "high", vulnerability.FixStateWontFix) row3 := mustRow(t, "pkgC", "3.0.0", "3.1.0", "os", "CVE-2023-9012", "medium", vulnerability.FixStateFixed) rs := rows{row1, row2, row3} result := rs.Render() expected := [][]string{ {"pkgA", "1.0.0", "", "os", "CVE-2023-1234", "critical", "3.0% (75th)", " N/A"}, {"pkgB", "2.0.0", "(won't fix)", "os", "CVE-2023-5678", "high", "3.0% (75th)", " N/A"}, {"pkgC", "3.0.0", "3.1.0", "os", "CVE-2023-9012", "medium", "3.0% (75th)", " N/A"}, } if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("Render() mismatch (-want +got):\n%s", diff) } }) t.Run("column count matches expectations", func(t *testing.T) { rs := rows{ mustRow(t, "pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", vulnerability.FixStateFixed), } result := rs.Render() expected := [][]string{ {"pkg1", "1.0.0", "1.1.0", "os", "CVE-2023-1234", "critical", "3.0% (75th)", " N/A"}, } if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("Render() mismatch (-want +got):\n%s", diff) } // expected columns: name, version, fix, packageType, vulnID, severity, epss, risk assert.Len(t, result[0], 8) }) } func createTestRow(name, version, fix, pkgType, vulnID, severity string, fixState vulnerability.FixState) (row, error) { m := models.Match{ Vulnerability: models.Vulnerability{ Fix: models.Fix{ Versions: []string{fix}, State: fixState.String(), }, VulnerabilityMetadata: models.VulnerabilityMetadata{ ID: vulnID, Severity: severity, Cvss: []models.Cvss{ { Source: "nvd", Type: "CVSS", Version: "3.1", Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:L", Metrics: models.CvssMetrics{ BaseScore: 7.2, }, }, }, EPSS: []models.EPSS{ { CVE: vulnID, EPSS: 0.03, Percentile: 0.75, }, }, }, }, Artifact: models.Package{ Name: name, Version: version, Type: syftPkg.Type(pkgType), }, } p := NewPresenter(models.PresenterConfig{}, false) r := p.newRow(m, "", false) return r, nil } func TestEPSS_String(t *testing.T) { tests := []struct { name string score float64 percentile float64 expected string }{ { name: "zero percentile should return N/A", score: 0.0, percentile: 0.0, expected: "N/A", }, { name: "very low probability less than 0.1%", score: 0.0005, percentile: 0.15, expected: "< 0.1% (15th)", }, { name: "low probability with 1st percentile", score: 0.02, percentile: 0.01, expected: "2.0% (1st)", }, { name: "medium probability with 2nd percentile", score: 0.153, percentile: 0.92, expected: "15.3% (92nd)", }, { name: "high probability with 3rd percentile", score: 0.456, percentile: 0.93, expected: "45.6% (93rd)", }, { name: "probability with 4th percentile", score: 0.234, percentile: 0.84, expected: "23.4% (84th)", }, { name: "probability with 11th percentile (special case)", score: 0.125, percentile: 0.11, expected: "12.5% (11th)", }, { name: "probability with 12th percentile (special case)", score: 0.187, percentile: 0.12, expected: "18.7% (12th)", }, { name: "probability with 13th percentile (special case)", score: 0.203, percentile: 0.13, expected: "20.3% (13th)", }, { name: "probability with 21st percentile", score: 0.312, percentile: 0.21, expected: "31.2% (21st)", }, { name: "probability with 22nd percentile", score: 0.345, percentile: 0.22, expected: "34.5% (22nd)", }, { name: "probability with 23rd percentile", score: 0.378, percentile: 0.23, expected: "37.8% (23rd)", }, { name: "high percentile with 99th", score: 0.789, percentile: 0.99, expected: "78.9% (99th)", }, { name: "maximum probability and percentile", score: 1.0, percentile: 1.0, expected: "100.0% (100th)", }, { name: "very small non-zero probability", score: 0.001, percentile: 0.05, expected: "0.1% (5th)", }, { name: "edge case: exactly 0.1% probability", score: 0.001, percentile: 0.08, expected: "0.1% (8th)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := epss{ Score: tt.score, Percentile: tt.percentile, } result := e.String() assert.Equal(t, tt.expected, result) }) } } func mustRow(t *testing.T, name, version, fix, pkgType, vulnID, severity string, fixState vulnerability.FixState) row { r, err := createTestRow(name, version, fix, pkgType, vulnID, severity, fixState) if err != nil { t.Fatalf("failed to create test row: %v", err) } return r } ================================================ FILE: grype/presenter/table/testdata/snapshot/TestTablePresenter_Color.golden ================================================ NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY package-1 1.1.1 the-next-version rpm CVE-1999-0001 Low package-2 2.2.2 deb CVE-1999-0002 Critical ================================================ FILE: grype/presenter/template/presenter.go ================================================ package template import ( "fmt" "io" "os" "reflect" "text/template" "github.com/Masterminds/sprig/v3" "github.com/anchore/clio" "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/presenter/models" ) // Presenter is an implementation of presenter.Presenter that formats output according to a user-provided Go text template. type Presenter struct { id clio.Identification document models.Document pathToTemplateFile string } // NewPresenter returns a new template.Presenter. func NewPresenter(pb models.PresenterConfig, templateFile string) *Presenter { return &Presenter{ id: pb.ID, document: pb.Document, pathToTemplateFile: templateFile, } } // Present creates output using a user-supplied Go template. func (pres *Presenter) Present(output io.Writer) error { expandedPathToTemplateFile, err := homedir.Expand(pres.pathToTemplateFile) if err != nil { return fmt.Errorf("unable to expand path %q", pres.pathToTemplateFile) } templateContents, err := os.ReadFile(expandedPathToTemplateFile) if err != nil { return fmt.Errorf("unable to get output template: %w", err) } templateName := expandedPathToTemplateFile tmpl, err := template.New(templateName).Funcs(FuncMap).Parse(string(templateContents)) if err != nil { return fmt.Errorf("unable to parse template: %w", err) } err = tmpl.Execute(output, pres.document) if err != nil { return fmt.Errorf("unable to execute supplied template: %w", err) } return nil } // FuncMap is a function that returns template.FuncMap with custom functions available to template authors. var FuncMap = func() template.FuncMap { f := sprig.HermeticTxtFuncMap() f["getLastIndex"] = func(collection interface{}) int { if v := reflect.ValueOf(collection); v.Kind() == reflect.Slice { return v.Len() - 1 } return 0 } f["byMatchName"] = func(collection interface{}) interface{} { matches, ok := collection.([]models.Match) if !ok { return collection } models.SortMatches(matches, models.SortByPackage) return matches } return f }() ================================================ FILE: grype/presenter/template/presenter_test.go ================================================ package template import ( "bytes" "flag" "os" "path" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/internal/testutils" ) var update = flag.Bool("update", false, "update the *.golden files for template presenters") func TestPresenter_Present(t *testing.T) { workingDirectory, err := os.Getwd() if err != nil { t.Fatal(err) } templateFilePath := path.Join(workingDirectory, "./testdata/test.template") pb := internal.GeneratePresenterConfig(t, internal.ImageSource) templatePresenter := NewPresenter(pb, templateFilePath) var buffer bytes.Buffer if err := templatePresenter.Present(&buffer); err != nil { t.Fatal(err) } actual := buffer.Bytes() if *update { testutils.UpdateGoldenFileContents(t, actual) } expected := testutils.GetGoldenFileContents(t) assert.Equal(t, string(expected), string(actual)) } func TestPresenter_SprigDate_Fails(t *testing.T) { workingDirectory, err := os.Getwd() require.NoError(t, err) // this template has the generic sprig date function, which is intentionally not supported for security reasons templateFilePath := path.Join(workingDirectory, "./testdata/test.template.sprig.date") pb := internal.GeneratePresenterConfig(t, internal.ImageSource) templatePresenter := NewPresenter(pb, templateFilePath) var buffer bytes.Buffer err = templatePresenter.Present(&buffer) require.ErrorContains(t, err, `function "now" not defined`) } ================================================ FILE: grype/presenter/template/testdata/snapshot/TestPresenter_Present.golden ================================================ Identified distro as centos version 8.0. Vulnerability: CVE-1999-0001 Severity: Low Package: package-1 version 1.1.1 (rpm) CPEs: ["cpe:2.3:a:anchore\\:oss:anchore\\/engine:0.9.2:*:*:en:*:*:*:*"] Matched by: dpkg-matcher Vulnerability: CVE-1999-0002 Severity: Critical Package: package-2 version 2.2.2 (deb) CPEs: ["cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*"] Matched by: dpkg-matcher ================================================ FILE: grype/presenter/template/testdata/test.template ================================================ Identified distro as {{.Distro.Name}} version {{.Distro.Version}}. {{- range .Matches}} Vulnerability: {{.Vulnerability.ID}} Severity: {{.Vulnerability.Severity}} Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}}) CPEs: {{ toJson .Artifact.CPEs }} {{- range .MatchDetails}} Matched by: {{.Matcher}} {{- end}} {{- end}} ================================================ FILE: grype/presenter/template/testdata/test.template.sprig.date ================================================ Identified distro as {{.Distro.Name}} version {{.Distro.Version}}. Date: {{ now | date "2006-01-02" }} {{- range .Matches}} Vulnerability: {{.Vulnerability.ID}} Severity: {{.Vulnerability.Severity}} Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}}) CPEs: {{ toJson .Artifact.CPEs }} {{- range .MatchDetails}} Matched by: {{.Matcher}} {{- end}} {{- end}} ================================================ FILE: grype/presenter/template/testdata/test.valid.template ================================================ Identified distro as {{.Distro.Name}} version {{.Distro.Version}}. {{- range .Matches}} Vulnerability: {{.Vulnerability.ID}} CVE: {{trimPrefix "CVE-" .Vulnerability.ID}} Severity: {{.Vulnerability.Severity}} Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}}) {{- range .MatchDetails}} Matched by: {{.Matcher}} {{- end}} {{- end}} ================================================ FILE: grype/search/cpe.go ================================================ package search import ( "fmt" "strings" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" ) var _ interface { vulnerability.Criteria } = (*CPECriteria)(nil) type CPECriteria struct { CPE cpe.CPE } // ByCPE returns criteria which will search based on any of the provided CPEs func ByCPE(c cpe.CPE) vulnerability.Criteria { return &CPECriteria{ CPE: c, } } func (v *CPECriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) { if containsCPE(vuln.CPEs, v.CPE) { return true, "", nil } return false, "CPE attributes do not match", nil } func (v *CPECriteria) Summarize() string { return fmt.Sprintf("does not match CPE: %s", v.CPE.Attributes.BindToFmtString()) } // containsCPE returns true if the provided slice contains a matching CPE based on attributes matching func containsCPE(cpes []cpe.CPE, cpe cpe.CPE) bool { for _, c := range cpes { if matchesAttributes(cpe.Attributes, c.Attributes) { return true } } return false } func matchesAttributes(a1 cpe.Attributes, a2 cpe.Attributes) bool { if !matchesAttribute(a1.Product, a2.Product) || !matchesAttribute(a1.Vendor, a2.Vendor) || !matchesAttribute(a1.Part, a2.Part) || !matchesAttribute(a1.Language, a2.Language) || !matchesAttribute(a1.SWEdition, a2.SWEdition) || !matchesAttribute(a1.TargetSW, a2.TargetSW) || !matchesAttribute(a1.TargetHW, a2.TargetHW) || !matchesAttribute(a1.Other, a2.Other) || !matchesAttribute(a1.Edition, a2.Edition) { return false } return true } func matchesAttribute(a1, a2 string) bool { return a1 == "" || a2 == "" || strings.EqualFold(a1, a2) } ================================================ FILE: grype/search/cpe_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" ) func Test_ByCPE(t *testing.T) { tests := []struct { name string cpe cpe.CPE input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", cpe: cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", ""), input: vulnerability.Vulnerability{ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", "")}, }, matches: true, }, { name: "not match", cpe: cpe.Must("cpe:2.3:a:a-vendor:b-product:*:*:*:*:*:*:*:*", ""), input: vulnerability.Vulnerability{ CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*", "")}, }, matches: false, reason: "CPE attributes do not match", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByCPE(tt.cpe) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } ================================================ FILE: grype/search/criteria.go ================================================ package search import ( "fmt" "iter" "reflect" "slices" "strings" "github.com/anchore/grype/grype/vulnerability" ) // ------- Utilities ------- // CriteriaIterator processes all conditions into distinct sets of flattened criteria func CriteriaIterator(criteria []vulnerability.Criteria) iter.Seq2[int, []vulnerability.Criteria] { if len(criteria) == 0 { return func(_ func(int, []vulnerability.Criteria) bool) {} } return func(yield func(int, []vulnerability.Criteria) bool) { idx := 0 fn := func(criteria []vulnerability.Criteria) bool { out := yield(idx, criteria) idx++ return out } _ = processRemaining(nil, criteria, fn) } } func processRemaining(row, criteria []vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool { if len(criteria) == 0 { return yield(row) } return processRemainingItem(row, criteria[1:], criteria[0], yield) } func processRemainingItem(row, criteria []vulnerability.Criteria, item vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool { switch item := item.(type) { case and: // we replace this criteria object with its constituent parts return processRemaining(row, append(item, criteria...), yield) case or: for _, option := range item { if !processRemainingItem(row, criteria, option, yield) { return false } } default: return processRemaining(append(row, item), criteria, yield) } return true // continue } var allowedMultipleCriteria = []reflect.Type{reflect.TypeOf(funcCriteria{})} // ValidateCriteria asserts that there are no incorrect duplications of criteria // e.g. multiple ByPackageName() which would result in no matches, while Or(pkgName1, pkgName2) is allowed func ValidateCriteria(criteria []vulnerability.Criteria) error { for _, row := range CriteriaIterator(criteria) { // process OR conditions into flattened lists of AND conditions seenTypes := make(map[reflect.Type]interface{}) for _, criterion := range row { criterionType := reflect.TypeOf(criterion) if slices.Contains(allowedMultipleCriteria, criterionType) { continue } if previous, exists := seenTypes[criterionType]; exists { return fmt.Errorf("multiple conflicting criteria specified: %+v %+v", previous, criterion) } seenTypes[criterionType] = criterion } } return nil } var _ interface { vulnerability.Criteria } = (*or)(nil) // orCriteria provides a way to specify multiple criteria to be used, only requiring one to match type or []vulnerability.Criteria func Or(criteria ...vulnerability.Criteria) vulnerability.Criteria { return or(criteria) } func (c or) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) { var reasons []string for _, crit := range c { matches, reason, err := crit.MatchesVulnerability(v) if matches || err != nil { return matches, reason, err } reasons = append(reasons, reason) } return false, fmt.Sprintf("any(%s)", strings.Join(reasons, "; ")), nil } var _ interface { vulnerability.Criteria } = (*and)(nil) // andCriteria provides a way to specify multiple criteria to be used, all required type and []vulnerability.Criteria func And(criteria ...vulnerability.Criteria) vulnerability.Criteria { return and(criteria) } func (c and) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) { var reasons []string for _, crit := range c { matches, reason, err := crit.MatchesVulnerability(v) if matches || err != nil { return matches, reason, err } reasons = append(reasons, reason) } return false, fmt.Sprintf("all(%s)", strings.Join(reasons, "; ")), nil } ================================================ FILE: grype/search/criteria_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/vulnerability" ) func Test_CriteriaIterator(t *testing.T) { name1 := ByPackageName("name1") name2 := ByPackageName("name2") name3 := ByPackageName("name3") tests := []struct { name string in []vulnerability.Criteria expected [][]vulnerability.Criteria }{ { name: "empty", in: nil, expected: nil, }, { name: "one", in: []vulnerability.Criteria{name1}, expected: [][]vulnerability.Criteria{{name1}}, }, { name: "name1 or name2", in: []vulnerability.Criteria{Or(name1, name2)}, expected: [][]vulnerability.Criteria{{name1}, {name2}}, }, { name: "name1 AND (name2 or name3)", in: []vulnerability.Criteria{name1, Or(name2, name3)}, expected: [][]vulnerability.Criteria{{name1, name2}, {name1, name3}}, }, { name: "name1 AND (name2 or name3) AND (name1 or name2 or name3)", in: []vulnerability.Criteria{name1, Or(name2, name3), Or(name1, name2, name3)}, expected: [][]vulnerability.Criteria{ {name1, name2, name1}, {name1, name3, name1}, {name1, name2, name2}, {name1, name3, name2}, {name1, name2, name3}, {name1, name3, name3}, }, }, { name: "(name1 AND name2) OR (name1 AND name3)", in: []vulnerability.Criteria{Or(And(name1, name2), And(name1, name3))}, expected: [][]vulnerability.Criteria{ {name1, name2}, {name1, name3}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var got [][]vulnerability.Criteria for _, row := range CriteriaIterator(test.in) { got = append(got, row) } require.ElementsMatch(t, test.expected, got) }) } } func Test_ValidateCriteria(t *testing.T) { tests := []struct { name string in []vulnerability.Criteria wantErr require.ErrorAssertionFunc }{ { name: "no error", in: []vulnerability.Criteria{ByPackageName("steve"), ByDistro(distro.Distro{})}, wantErr: require.NoError, }, { name: "package name error", in: []vulnerability.Criteria{ByPackageName("steve"), ByPackageName("bob")}, wantErr: require.Error, }, { name: "multiple distros error", in: []vulnerability.Criteria{ByDistro(distro.Distro{}), ByDistro(distro.Distro{})}, wantErr: require.Error, }, { name: "multiple package name in or condition not error", in: []vulnerability.Criteria{Or(ByPackageName("steve"), ByPackageName("bob"))}, wantErr: require.NoError, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { err := ValidateCriteria(test.in) test.wantErr(t, err) }) } } ================================================ FILE: grype/search/distro.go ================================================ package search import ( "fmt" "strings" "github.com/anchore/grype/grype/db/v5/namespace" distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/vulnerability" ) // ByDistro returns criteria which will match vulnerabilities based on any of the provided Distros func ByDistro(d ...distro.Distro) vulnerability.Criteria { return &DistroCriteria{ Distros: d, Exact: false, } } // ByExactDistro returns criteria which will match vulnerabilities based on any of the provided Distros // without applying alias mappings. This is useful when you need to find records specific to a distro // that would normally be aliased to another (e.g., AlmaLinux-specific records vs RHEL records). func ByExactDistro(d ...distro.Distro) vulnerability.Criteria { return &DistroCriteria{ Distros: d, Exact: true, } } type DistroCriteria struct { Distros []distro.Distro Exact bool // if true, disable alias mappings (e.g., AlmaLinux -> RHEL) } func (c *DistroCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { ns, err := namespace.FromString(value.Namespace) if err != nil { return false, fmt.Sprintf("unable to determine namespace for vulnerability %v: %v", value.ID, err), nil } dns, ok := ns.(*distroNs.Namespace) if !ok || dns == nil { // not a Distro-based vulnerability return false, "not a distro-based vulnerability", nil } if len(c.Distros) == 0 { return true, "", nil } var distroStrs []string for _, d := range c.Distros { var matches bool if c.Exact { matches = matchesExactDistro(&d, dns) } else { matches = matchesDistro(&d, dns) } if matches { return true, "", nil } distroStrs = append(distroStrs, d.String()) } return false, fmt.Sprintf("does not match any known distro: %q", strings.Join(distroStrs, ", ")), nil } func (c *DistroCriteria) Summarize() string { var distroStrs []string for _, d := range c.Distros { distroStrs = append(distroStrs, d.String()) } return "does not match distro(s): " + strings.Join(distroStrs, ", ") } var _ interface { vulnerability.Criteria } = (*DistroCriteria)(nil) // matchesDistro returns true distro types are equal and versions are compatible func matchesDistro(d *distro.Distro, ns *distroNs.Namespace) bool { if d == nil || ns == nil { return false } distroType := mimicV6DistroTypeOverrides(ns.DistroType()) targetType := mimicV6DistroTypeOverrides(d.Type) if distroType != targetType { return false } return compatibleVersion(d.Version, ns.Version()) } // compatibleVersion returns true when the versions are the same or the partial version describes the matching parts // of the fullVersion func compatibleVersion(fullVersion string, partialVersion string) bool { if fullVersion == "" { return true } if fullVersion == partialVersion { return true } if strings.HasPrefix(fullVersion, partialVersion) && len(fullVersion) > len(partialVersion) && fullVersion[len(partialVersion)] == '.' { return true } return false } // TODO: this is a temporary workaround... in the long term the mock should more strongly enforce // data overrides and not require this kind of logic being baked into mocks directly. func mimicV6DistroTypeOverrides(t distro.Type) distro.Type { overrideMap := map[string]string{ "centos": "rhel", "rocky": "rhel", "rockylinux": "rhel", "alma": "rhel", "almalinux": "rhel", "gentoo": "rhel", "archlinux": "arch", "oracle": "ol", "oraclelinux": "ol", "amazon": "amzn", "amazonlinux": "amzn", } applyMapping := func(i string) distro.Type { if replacement, exists := distro.IDMapping[i]; exists { return replacement } return distro.Type(i) } if replacement, exists := overrideMap[string(t)]; exists { return applyMapping(replacement) } return applyMapping(string(t)) } // matchesExactDistro returns true when distro types are equal and versions are compatible, // without applying any alias mappings func matchesExactDistro(d *distro.Distro, ns *distroNs.Namespace) bool { if d == nil || ns == nil { return false } // Compare distro types directly without any alias mappings if string(d.Type) != string(ns.DistroType()) { return false } return compatibleVersion(d.Version, ns.Version()) } ================================================ FILE: grype/search/distro_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/vulnerability" ) func Test_ByDistro(t *testing.T) { deb8 := distro.New(distro.Debian, "8", "") tests := []struct { name string distro distro.Distro input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", distro: *deb8, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "debian:distro:debian:8", }, }, matches: true, }, { name: "not match", distro: *deb8, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "debian:distro:ubuntu:8", }, }, matches: false, reason: `does not match any known distro: "debian 8"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByDistro(tt.distro) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } func Test_ByExactDistro(t *testing.T) { alma8 := distro.New(distro.AlmaLinux, "8", "") rhel8 := distro.New(distro.RedHat, "8", "") tests := []struct { name string distro distro.Distro input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "exact match - AlmaLinux", distro: *alma8, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "almalinux:distro:almalinux:8", }, }, matches: true, }, { name: "no alias mapping - AlmaLinux vs RHEL", distro: *alma8, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "redhat:distro:redhat:8", }, }, matches: false, reason: `does not match any known distro: "almalinux 8"`, }, { name: "exact match - RHEL", distro: *rhel8, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "redhat:distro:redhat:8", }, }, matches: true, }, { name: "no alias mapping - RHEL vs AlmaLinux", distro: *rhel8, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "almalinux:distro:almalinux:8", }, }, matches: false, reason: `does not match any known distro: "rhel 8"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByExactDistro(tt.distro) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } func Test_ByDistro_vs_ByExactDistro_AliasMapping(t *testing.T) { alma8 := distro.New(distro.AlmaLinux, "8", "") rhelVuln := vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "redhat:distro:redhat:8", }, } // Test that ByDistro applies alias mapping (AlmaLinux -> RHEL) regularConstraint := ByDistro(*alma8) matches, _, err := regularConstraint.MatchesVulnerability(rhelVuln) require.NoError(t, err) assert.True(t, matches, "ByDistro should match RHEL vulns for AlmaLinux due to alias mapping") // Test that ByExactDistro does NOT apply alias mapping exactConstraint := ByExactDistro(*alma8) matches, _, err = exactConstraint.MatchesVulnerability(rhelVuln) require.NoError(t, err) assert.False(t, matches, "ByExactDistro should NOT match RHEL vulns for AlmaLinux (no alias mapping)") } ================================================ FILE: grype/search/ecosystem.go ================================================ package search import ( "fmt" "github.com/anchore/grype/grype/db/v5/namespace" "github.com/anchore/grype/grype/db/v5/namespace/language" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) // ByEcosystem returns criteria which will search based on the package Language and or package type func ByEcosystem(lang syftPkg.Language, t syftPkg.Type) vulnerability.Criteria { return &EcosystemCriteria{ Language: lang, PackageType: t, } } type EcosystemCriteria struct { Language syftPkg.Language PackageType syftPkg.Type } func (c *EcosystemCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { ns, err := namespace.FromString(value.Namespace) if err != nil { return false, fmt.Sprintf("unable to determine namespace for vulnerability %v: %v", value.ID, err), nil } lang, ok := ns.(*language.Namespace) if !ok || lang == nil { // not a language-based vulnerability return false, "not a language-based vulnerability", nil } // TODO: add package type? vulnLanguage := lang.Language() matchesLanguage := c.Language == vulnLanguage if !matchesLanguage { return false, fmt.Sprintf("vulnerability language %q does not match package language %q", vulnLanguage, c.Language), nil } return true, "", nil } var _ interface { vulnerability.Criteria } = (*EcosystemCriteria)(nil) ================================================ FILE: grype/search/ecosystem_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" ) func Test_ByLanguage(t *testing.T) { tests := []struct { name string lang syftPkg.Language pkgType syftPkg.Type input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", lang: syftPkg.Java, pkgType: syftPkg.JavaPkg, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "github:language:java", }, }, matches: true, }, { name: "not match", lang: syftPkg.Java, pkgType: syftPkg.JavaPkg, input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ Namespace: "github:language:javascript", }, }, matches: false, reason: `vulnerability language "javascript" does not match package language "java"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByEcosystem(tt.lang, tt.pkgType) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } ================================================ FILE: grype/search/func.go ================================================ package search import "github.com/anchore/grype/grype/vulnerability" // ByFunc returns criteria which will use the provided function to filter vulnerabilities func ByFunc(criteriaFunc func(vulnerability.Vulnerability) (bool, string, error)) vulnerability.Criteria { return funcCriteria{fn: criteriaFunc} } // funcCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability type funcCriteria struct { fn func(vulnerability.Vulnerability) (bool, string, error) } func (f funcCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { return f.fn(value) } var _ vulnerability.Criteria = (*funcCriteria)(nil) ================================================ FILE: grype/search/func_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func Test_ByFunc(t *testing.T) { tests := []struct { name string fn func(vulnerability.Vulnerability) (bool, string, error) input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", fn: func(v vulnerability.Vulnerability) (bool, string, error) { return true, "", nil }, input: vulnerability.Vulnerability{}, matches: true, }, { name: "not match", fn: func(v vulnerability.Vulnerability) (bool, string, error) { return false, "reason!", nil }, input: vulnerability.Vulnerability{}, matches: false, reason: "reason!", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByFunc(tt.fn) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } ================================================ FILE: grype/search/id.go ================================================ package search import ( "fmt" "github.com/anchore/grype/grype/vulnerability" ) // ByID returns criteria to search by vulnerability ID, such as CVE-2024-9143 func ByID(id string) vulnerability.Criteria { return &IDCriteria{ ID: id, } } // IDCriteria is able to match vulnerabilities to the assigned ID, such as CVE-2024-1000 or GHSA-g2x7-ar59-85z5 type IDCriteria struct { ID string } func (v *IDCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) { matchesID := vuln.ID == v.ID if !matchesID { return false, fmt.Sprintf("vulnerability ID %q does not match expected ID %q", vuln.ID, v.ID), nil } return true, "", nil } var _ interface { vulnerability.Criteria } = (*IDCriteria)(nil) ================================================ FILE: grype/search/id_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func Test_ByID(t *testing.T) { tests := []struct { name string id string input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", id: "CVE-YEAR-1", input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-YEAR-1", }, }, matches: true, }, { name: "not match", id: "CVE-YEAR-1", input: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-YEAR-2", }, }, matches: false, reason: `vulnerability ID "CVE-YEAR-2" does not match expected ID "CVE-YEAR-1"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByID(tt.id) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } ================================================ FILE: grype/search/package_name.go ================================================ package search import ( "fmt" "strings" "github.com/anchore/grype/grype/vulnerability" ) // ByPackageName returns criteria restricting vulnerabilities to match the package name provided func ByPackageName(packageName string) vulnerability.Criteria { return &PackageNameCriteria{ PackageName: packageName, } } type PackageNameCriteria struct { PackageName string } func (v *PackageNameCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) { matchesPackageName := strings.EqualFold(vuln.PackageName, v.PackageName) if !matchesPackageName { return false, fmt.Sprintf("vulnerability package name %q does not match expected package name %q", vuln.PackageName, v.PackageName), nil } return true, "", nil } var _ interface { vulnerability.Criteria } = (*PackageNameCriteria)(nil) ================================================ FILE: grype/search/package_name_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func Test_ByPackageName(t *testing.T) { tests := []struct { name string packageName string input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", packageName: "some-name", input: vulnerability.Vulnerability{ PackageName: "some-name", }, matches: true, }, { name: "match case insensitive", packageName: "some-name", input: vulnerability.Vulnerability{ PackageName: "SomE-NaMe", }, matches: true, }, { name: "not match", packageName: "some-name", input: vulnerability.Vulnerability{ PackageName: "other-name", }, matches: false, reason: `vulnerability package name "other-name" does not match expected package name "some-name"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByPackageName(tt.packageName) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } ================================================ FILE: grype/search/unaffected.go ================================================ package search import ( "github.com/anchore/grype/grype/vulnerability" ) var _ vulnerability.Criteria = (*UnaffectedCriteria)(nil) // ForUnaffected returns criteria which will cause the search to be against unaffected packages / vulnerabilities. func ForUnaffected() vulnerability.Criteria { return &UnaffectedCriteria{} } type UnaffectedCriteria struct { UnaffectedValue bool } func (c *UnaffectedCriteria) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) { // unaffected criteria filtering happens at the store level, so all vulnerabilities returned // from unaffected stores _should_ already match this criteria. Boolean indicator is a backup // sanity check. return v.Unaffected, "", nil } ================================================ FILE: grype/search/version_constraint.go ================================================ package search import ( "errors" "fmt" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/log" ) var _ interface { vulnerability.Criteria VersionConstraintMatcher } = (*VersionCriteria)(nil) // VersionConstraintMatcher is used for searches which include version.Constraints; this should be used instead of // post-filtering vulnerabilities in order to most efficiently hydrate data in memory type VersionConstraintMatcher interface { MatchesConstraint(constraint version.Constraint) (bool, error) } // ByConstraintFunc returns criteria which will use the provided function as inclusion criteria func ByConstraintFunc(constraintFunc func(constraint version.Constraint) (bool, error)) vulnerability.Criteria { return &constraintFuncCriteria{fn: constraintFunc} } type VersionCriteria struct { Version version.Version } func (v VersionCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { return ByConstraintFunc(v.criteria).MatchesVulnerability(value) } func (v VersionCriteria) MatchesConstraint(constraint version.Constraint) (bool, error) { return v.criteria(constraint) } func (v VersionCriteria) criteria(constraint version.Constraint) (bool, error) { // The config is now embedded in the version itself, so just call Satisfied satisfied, err := constraint.Satisfied(&v.Version) if err != nil { var unsupportedError *version.UnsupportedComparisonError if errors.As(err, &unsupportedError) { // if the format is unsupported, then the constraint is not satisfied, but this should not be conveyed as an error log.WithFields("reason", err).Trace("unsatisfied constraint") return false, nil } var e *version.NonFatalConstraintError if errors.As(err, &e) { log.Warn(e) } else { return false, fmt.Errorf("failed to check constraint=%v version=%v: %w", constraint, v, err) } } return satisfied, nil } // ByFixedVersion returns criteria which constrains vulnerabilities to those that are fixed based on the provided version, // in other words: vulnerabilities where the fix version is less than v func ByFixedVersion(v version.Version) vulnerability.Criteria { return &funcCriteria{ func(vuln vulnerability.Vulnerability) (bool, string, error) { var err error if vuln.Fix.State != vulnerability.FixStateFixed { return false, "", nil } for _, fixVersion := range vuln.Fix.Versions { cmp, e := version.New(fixVersion, v.Format).Compare(&v) if e != nil { err = e } if cmp <= 0 { // fix version is less than or equal to the provided version, so is considered fixed return true, fmt.Sprintf("fix version %v is less than %v", v, fixVersion), err } } return false, "", err }, } } // ByVersion returns criteria which constrains vulnerabilities to those with matching version constraints func ByVersion(v version.Version) vulnerability.Criteria { return &VersionCriteria{ Version: v, } } // constraintFuncCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability type constraintFuncCriteria struct { fn func(constraint version.Constraint) (bool, error) summary string } func (f *constraintFuncCriteria) MatchesConstraint(constraint version.Constraint) (bool, error) { return f.fn(constraint) } func (f *constraintFuncCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) { if value.Constraint == nil { // if there is no constraint, then we cannot match against it return false, "no version constraint", nil } matches, err := f.fn(value.Constraint) // TODO: should we do something about this? return matches, "", err } func (f *constraintFuncCriteria) Summarize() string { return f.summary } var _ interface { vulnerability.Criteria VersionConstraintMatcher } = (*constraintFuncCriteria)(nil) func MultiConstraintMatcher(a, b VersionConstraintMatcher) VersionConstraintMatcher { return &multiConstraintMatcher{ a: a, b: b, } } // multiConstraintMatcher is used internally when multiple version constraint matchers are specified type multiConstraintMatcher struct { a, b VersionConstraintMatcher } func (m *multiConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) { a, err := m.a.MatchesConstraint(constraint) if a || err != nil { return a, err } return m.b.MatchesConstraint(constraint) } var _ interface { VersionConstraintMatcher } = (*multiConstraintMatcher)(nil) ================================================ FILE: grype/search/version_constraint_test.go ================================================ package search import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" ) func Test_ByVersion(t *testing.T) { tests := []struct { name string version string input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", version: "1.0", input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), }, matches: true, }, { name: "not match", version: "2.0", input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), }, matches: false, reason: "", // we don't expect a reason to be raised up at this level }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := version.New(tt.version, version.SemanticFormat) constraint := ByVersion(*v) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } func Test_ByConstraintFunc(t *testing.T) { tests := []struct { name string constraintFunc func(version.Constraint) (bool, error) input vulnerability.Vulnerability wantErr require.ErrorAssertionFunc matches bool reason string }{ { name: "match", constraintFunc: func(version.Constraint) (bool, error) { return true, nil }, input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), }, matches: true, }, { name: "not match", constraintFunc: func(version.Constraint) (bool, error) { return false, nil }, input: vulnerability.Vulnerability{ Constraint: version.MustGetConstraint("< 2.0", version.SemanticFormat), }, matches: false, reason: "", // we don't expect a reason to be raised up at this level }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { constraint := ByConstraintFunc(tt.constraintFunc) matches, reason, err := constraint.MatchesVulnerability(tt.input) wantErr := require.NoError if tt.wantErr != nil { wantErr = tt.wantErr } wantErr(t, err) assert.Equal(t, tt.matches, matches) assert.Equal(t, tt.reason, reason) }) } } func Test_ByFixedVersion(t *testing.T) { tests := []struct { name string version string input vulnerability.Vulnerability matches bool }{ { name: "fixed version is lower", version: "1.1.0", input: vulnerability.Vulnerability{ Fix: vulnerability.Fix{ Versions: []string{"1.0.0"}, State: vulnerability.FixStateFixed, }, }, matches: true, }, { name: "fixed version is equal", version: "1.1.0", input: vulnerability.Vulnerability{ Fix: vulnerability.Fix{ Versions: []string{"1.1.0"}, State: vulnerability.FixStateFixed, }, }, matches: true, }, { name: "one of multiple fix versions matches", version: "1.1.0", input: vulnerability.Vulnerability{ Fix: vulnerability.Fix{ Versions: []string{"1.0.0", "1.2.0"}, State: vulnerability.FixStateFixed, }, }, matches: true, }, { name: "fixed version is higher", version: "1.1.0", input: vulnerability.Vulnerability{ Fix: vulnerability.Fix{ Versions: []string{"1.2.0"}, State: vulnerability.FixStateFixed, }, }, matches: false, }, { name: "no fix versions", version: "1.1.0", input: vulnerability.Vulnerability{ Fix: vulnerability.Fix{ Versions: []string{}, State: vulnerability.FixStateWontFix, }, }, matches: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := version.New(tt.version, version.SemanticFormat) constraint := ByFixedVersion(*v) matches, reason, err := constraint.MatchesVulnerability(tt.input) require.NoError(t, err) assert.Equal(t, tt.matches, matches) if matches { assert.NotEmpty(t, reason) } else { assert.Empty(t, reason) } }) } } ================================================ FILE: grype/version/apk_version.go ================================================ package version import ( apk "github.com/knqyf263/go-apk-version" ) var _ Comparator = (*apkVersion)(nil) type apkVersion struct { obj apk.Version } func newApkVersion(raw string) (apkVersion, error) { ver, err := apk.NewVersion(trimLeadingV(raw)) if err != nil { return apkVersion{}, invalidFormatError(ApkFormat, raw, err) } return apkVersion{ obj: ver, }, nil } // trimLeadingV removes a single leading 'v' or 'V' prefix only if it's followed by a digit. // This allows versions like "v1.5.0" to be treated as "1.5.0" while preserving other strings as-is. func trimLeadingV(raw string) string { if len(raw) >= 2 && (raw[0] == 'v' || raw[0] == 'V') && raw[1] >= '0' && raw[1] <= '9' { return raw[1:] } return raw } func (v apkVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } apkVer, err := newApkVersion(other.Raw) if err != nil { return -1, err } return v.obj.Compare(apkVer.obj), nil } ================================================ FILE: grype/version/apk_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func intRef(i int) *int { return &i } func TestTrimLeadingV(t *testing.T) { tests := []struct { name string input string want string }{ { name: "lowercase v followed by digit", input: "v1.5.0", want: "1.5.0", }, { name: "uppercase V followed by digit", input: "V1.5.0", want: "1.5.0", }, { name: "no prefix", input: "1.5.0", want: "1.5.0", }, { name: "v not followed by digit", input: "version1.0", want: "version1.0", }, { name: "empty string", input: "", want: "", }, { name: "single v", input: "v", want: "v", }, { name: "v followed by non-digit", input: "va.b.c", want: "va.b.c", }, { name: "double v prefix", input: "vv1.0.0", want: "vv1.0.0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := trimLeadingV(tt.input) assert.Equal(t, tt.want, got) }) } } func TestApkVersion_Constraint(t *testing.T) { tests := []testCase{ {version: "2.3.1", constraint: "", satisfied: true}, // compound conditions {version: "2.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: true}, {version: "2.0.0", constraint: "> 1.0.0, <= 2.0.0", satisfied: true}, {version: "2.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.0.0", constraint: ">= 1.0.0, < 2.0.0", satisfied: true}, {version: "1.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "0.9.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, // fixed-in scenarios {version: "2.3.1", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "< 2.0", satisfied: false}, {version: "2.3.1", constraint: "< 2", satisfied: false}, {version: "2.3.1", constraint: "< 2.3", satisfied: false}, {version: "2.3.1", constraint: "< 2.3.1", satisfied: false}, {version: "2.3.1", constraint: "< 2.3.2", satisfied: true}, {version: "2.3.1", constraint: "< 2.4", satisfied: true}, {version: "2.3.1", constraint: "< 3", satisfied: true}, {version: "2.3.1", constraint: "< 3.0", satisfied: true}, {version: "2.3.1", constraint: "< 3.0.0", satisfied: true}, // alpine specific scenarios // https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver {version: "1.5.1-r1", constraint: "< 1.5.1", satisfied: false}, {version: "1.5.1-r1", constraint: "> 1.5.1", satisfied: true}, {version: "9.3.2-r4", constraint: "< 9.3.4-r2", satisfied: true}, {version: "9.3.4-r2", constraint: "> 9.3.4", satisfied: true}, {version: "4.2.52_p2-r1", constraint: "< 4.2.52_p4-r2", satisfied: true}, {version: "4.2.52_p2-r1", constraint: "> 4.2.52_p4-r2", satisfied: false}, {version: "0.1.0_alpha", constraint: "< 0.1.3_alpha", satisfied: true}, {version: "0.1.0_alpha2", constraint: "> 0.1.0_alpha", satisfied: true}, {version: "1.1", constraint: "> 1.1_alpha1", satisfied: true}, {version: "1.1", constraint: "< 1.1_alpha1", satisfied: false}, {version: "2.3.0b-r1", constraint: "< 2.3.0b-r2", satisfied: true}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, ApkFormat) assert.NoError(t, err, "unexpected error from newApkConstraint: %v", err) test.assertVersionConstraint(t, ApkFormat, constraint) }) } } func TestApkVersion_Compare(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string expectResult *int // nil means don't check specific result }{ { name: "same format successful comparison", thisVersion: "1.2.3-r4", otherVersion: "1.2.3-r5", otherFormat: ApkFormat, expectError: false, }, { name: "different format does not return error", thisVersion: "1.2.3-r4", otherVersion: "1.2.3", otherFormat: SemanticFormat, expectError: false, }, { name: "different format does not return error - deb", thisVersion: "1.2.3-r4", otherVersion: "1.2.3-1", otherFormat: DebFormat, expectError: false, errorSubstring: "unsupported version comparison", }, { name: "unknown format attempts upgrade - valid apk format", thisVersion: "1.2.3-r4", otherVersion: "1.2.3-r5", otherFormat: UnknownFormat, expectError: false, }, { name: "unknown format attempts upgrade - invalid apk format", thisVersion: "1.2.3-r4", otherVersion: "not-valid-apk-format", otherFormat: UnknownFormat, expectError: true, errorSubstring: "invalid version", }, { name: "lowercase v prefix on other version is stripped", thisVersion: "1.5.0", otherVersion: "v1.5.0", otherFormat: ApkFormat, expectResult: intRef(0), }, { name: "uppercase V prefix on other version is stripped", thisVersion: "1.5.0", otherVersion: "V1.5.0", otherFormat: ApkFormat, expectResult: intRef(0), }, { name: "v prefix on this version is stripped", thisVersion: "v1.5.0", otherVersion: "1.5.0", otherFormat: ApkFormat, expectResult: intRef(0), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, err := newApkVersion(test.thisVersion) require.NoError(t, err) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) if test.expectResult != nil { assert.Equal(t, *test.expectResult, result) } else { assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } } }) } } func TestApkVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3-r4", ApkFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/bitnami_version.go ================================================ package version import ( "fmt" "strings" bitnami "github.com/bitnami/go-version/pkg/version" hashiVer "github.com/anchore/go-version" ) var _ Comparator = (*bitnamiVersion)(nil) type bitnamiVersion struct { obj *hashiVer.Version } func newBitnamiVersion(raw string) (bitnamiVersion, error) { bv, err := bitnami.Parse(raw) if err != nil { fmtErr := err verObj, err := hashiVer.NewVersion(raw) if err != nil { return bitnamiVersion{}, invalidFormatError(BitnamiFormat, raw, fmtErr) } var segments []string for _, segment := range verObj.Segments() { segments = append(segments, fmt.Sprintf("%d", segment)) } // drop any pre-release info raw = strings.Join(segments, ".") } else { raw = fmt.Sprintf("%d.%d.%d", bv.Major(), bv.Minor(), bv.Patch()) } // We can't assume Bitnami revisions can potentially address a // known vulnerability given Bitnami package revisions use // exactly the same upstream source code used to create the // previous version. Then, we discard it. verObj, err := hashiVer.NewVersion(raw) if err != nil { return bitnamiVersion{}, invalidFormatError(BitnamiFormat, raw, err) } return bitnamiVersion{ obj: verObj, }, nil } func (v bitnamiVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } bv, err := newBitnamiVersion(other.Raw) if err != nil { return 0, err } return v.obj.Compare(bv.obj), nil } ================================================ FILE: grype/version/bitnami_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBitnamiVersion_Constraint(t *testing.T) { tests := []testCase{ // empty values {version: "2.3.1", constraint: "", satisfied: true}, // typical cases {version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "2.3.1", satisfied: true}, {version: "2.3.1", constraint: "= 2.3.1", satisfied: true}, {version: "2.3.1", constraint: " = 2.3.1", satisfied: true}, {version: "2.3.1", constraint: ">= 2.3.1", satisfied: true}, {version: "2.3.1", constraint: "> 2.0.0", satisfied: true}, {version: "2.3.1", constraint: "> 2.0", satisfied: true}, {version: "2.3.1", constraint: "> 2", satisfied: true}, {version: "2.3.1", constraint: "> 2, < 3", satisfied: true}, {version: "2.3.1", constraint: "> 2.3, < 3.1", satisfied: true}, {version: "2.3.1", constraint: "> 2.3.0, < 3.1", satisfied: true}, {version: "2.3.1", constraint: ">= 2.3.1, < 3.1", satisfied: true}, {version: "2.3.1", constraint: " = 2.3.2", satisfied: false}, {version: "2.3.1", constraint: ">= 2.3.2", satisfied: false}, {version: "2.3.1", constraint: "> 2.3.1", satisfied: false}, {version: "2.3.1", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "< 2.0", satisfied: false}, {version: "2.3.1", constraint: "< 2", satisfied: false}, {version: "2.3.1", constraint: "< 2, > 3", satisfied: false}, {version: "2.3.1-1", constraint: "2.3.1", satisfied: true}, {version: "2.3.1-1", constraint: "= 2.3.1", satisfied: true}, {version: "2.3.1-1", constraint: " = 2.3.1", satisfied: true}, {version: "2.3.1-1", constraint: ">= 2.3.1", satisfied: true}, {version: "2.3.1-1", constraint: "> 2.0.0", satisfied: true}, {version: "2.3.1-1", constraint: "> 2.0", satisfied: true}, {version: "2.3.1-1", constraint: "> 2", satisfied: true}, {version: "2.3.1-1", constraint: "> 2, < 3", satisfied: true}, {version: "2.3.1-1", constraint: "> 2.3, < 3.1", satisfied: true}, {version: "2.3.1-1", constraint: "> 2.3.0, < 3.1", satisfied: true}, {version: "2.3.1-1", constraint: ">= 2.3.1, < 3.1", satisfied: true}, {version: "2.3.1-1", constraint: " = 2.3.2", satisfied: false}, {version: "2.3.1-1", constraint: ">= 2.3.2", satisfied: false}, {version: "2.3.1-1", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1-1", constraint: "< 2.0", satisfied: false}, {version: "2.3.1-1", constraint: "< 2", satisfied: false}, {version: "2.3.1-1", constraint: "< 2, > 3", satisfied: false}, // ignoring revisions {version: "2.3.1-1", constraint: "> 2.3.1", satisfied: false}, {version: "2.3.1-1", constraint: "< 2.3.1-2", satisfied: false}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, BitnamiFormat) require.NoError(t, err) test.assertVersionConstraint(t, BitnamiFormat, constraint) }) } } func TestBitnamiVersion_Compare(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3-4", otherVersion: "1.2.3-5", otherFormat: BitnamiFormat, expectError: false, }, { name: "semantic versioning successful comparison", thisVersion: "1.2.3-4", otherVersion: "1.2.3", otherFormat: SemanticFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid semver format", thisVersion: "1.2.3-4", otherVersion: "1.2.3-5", otherFormat: UnknownFormat, expectError: false, }, { name: "unknown format attempts upgrade - invalid semver format", thisVersion: "1.2.3-4", otherVersion: "not-valid-semver-format", otherFormat: UnknownFormat, expectError: true, errorSubstring: "invalid semantic version", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, err := newBitnamiVersion(test.thisVersion) require.NoError(t, err) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestBitnamiVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3-4", BitnamiFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/combined_constraint.go ================================================ package version import ( "fmt" "strings" "github.com/scylladb/go-set/strset" ) func CombineConstraints(constraints ...Constraint) Constraint { constraints = uniqueConstraints(constraints...) if len(constraints) == 0 { return nil } if len(constraints) == 1 { return constraints[0] } return combinedConstraint{ OrOperands: constraints, } } type combinedConstraint struct { OrOperands []Constraint } func (c combinedConstraint) String() string { return fmt.Sprintf("%s (%s)", c.Value(), strings.ToLower(c.Format().String())) } func (c combinedConstraint) Value() string { // TODO: there is room for improvement here to make this more readable (filter out redundant constraints... e.g. <1.0 || < 2.0 should just be < 2.0) var str string for i, op := range c.OrOperands { if i > 0 { str += " || " } str += op.Value() } return str } func (c combinedConstraint) Format() Format { format := UnknownFormat if len(c.OrOperands) > 0 { format = c.OrOperands[0].Format() } return format } func (c combinedConstraint) Satisfied(version *Version) (bool, error) { if version == nil { return false, fmt.Errorf("cannot evaluate combined constraint with nil version") } for _, op := range c.OrOperands { satisfied, err := op.Satisfied(version) if err != nil { return false, fmt.Errorf("error evaluating constraint %s: %w", op, err) } if satisfied { return true, nil } } return false, nil } func uniqueConstraints(constraints ...Constraint) []Constraint { var nonNil []Constraint seen := strset.New() for _, c := range constraints { if c == nil || seen.Has(c.Value()) { continue } seen.Add(c.Value()) nonNil = append(nonNil, c) } return nonNil } ================================================ FILE: grype/version/combined_constraint_test.go ================================================ package version import ( "errors" "strings" "testing" "github.com/stretchr/testify/require" ) func TestCombineConstraints(t *testing.T) { tests := []struct { name string constraints []Constraint want Constraint }{ { name: "no constraints returns nil", constraints: []Constraint{}, want: nil, }, { name: "single constraint returns same constraint", constraints: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), }, want: MustGetConstraint(">= 1.0.0", SemanticFormat), }, { name: "multiple constraints returns combined constraint", constraints: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), MustGetConstraint("< 2.0.0", SemanticFormat), }, want: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), MustGetConstraint("< 2.0.0", SemanticFormat), }, }, }, { name: "nil constraints are filtered out", constraints: []Constraint{ nil, MustGetConstraint(">= 1.0.0", SemanticFormat), nil, }, want: MustGetConstraint(">= 1.0.0", SemanticFormat), }, { name: "duplicate constraints are filtered out", constraints: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), MustGetConstraint(">= 1.0.0", SemanticFormat), MustGetConstraint("< 2.0.0", SemanticFormat), }, want: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), MustGetConstraint("< 2.0.0", SemanticFormat), }, }, }, { name: "all nil constraints returns nil", constraints: []Constraint{ nil, nil, }, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := CombineConstraints(tt.constraints...) require.Equal(t, tt.want, got) }) } } func TestCombinedConstraint_Methods(t *testing.T) { tests := []struct { name string constraint combinedConstraint version *Version wantValue string wantString string wantFormat Format wantSatisfied bool wantErr require.ErrorAssertionFunc }{ { name: "single operand semantic constraint satisfied", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), }, }, version: New("1.5.0", SemanticFormat), wantValue: ">= 1.0.0", wantString: ">= 1.0.0 (semantic)", wantFormat: SemanticFormat, wantSatisfied: true, }, { name: "single operand semantic constraint not satisfied", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 2.0.0", SemanticFormat), }, }, version: New("1.5.0", SemanticFormat), wantValue: ">= 2.0.0", wantString: ">= 2.0.0 (semantic)", wantFormat: SemanticFormat, wantSatisfied: false, }, { name: "multiple operands with OR logic - first satisfies", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), MustGetConstraint(">= 3.0.0", SemanticFormat), }, }, version: New("1.5.0", SemanticFormat), wantValue: ">= 1.0.0 || >= 3.0.0", wantString: ">= 1.0.0 || >= 3.0.0 (semantic)", wantFormat: SemanticFormat, wantSatisfied: true, }, { name: "multiple operands with OR logic - second satisfies", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 2.0.0", SemanticFormat), MustGetConstraint("< 2.0.0", SemanticFormat), }, }, version: New("1.5.0", SemanticFormat), wantValue: ">= 2.0.0 || < 2.0.0", wantString: ">= 2.0.0 || < 2.0.0 (semantic)", wantFormat: SemanticFormat, wantSatisfied: true, }, { name: "multiple operands with OR logic - none satisfy", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 2.0.0", SemanticFormat), MustGetConstraint(">= 3.0.0", SemanticFormat), }, }, version: New("1.5.0", SemanticFormat), wantValue: ">= 2.0.0 || >= 3.0.0", wantString: ">= 2.0.0 || >= 3.0.0 (semantic)", wantFormat: SemanticFormat, wantSatisfied: false, }, { name: "empty operands returns unknown format", constraint: combinedConstraint{ OrOperands: []Constraint{}, }, version: New("1.5.0", SemanticFormat), wantValue: "", wantString: " (unknown)", wantFormat: UnknownFormat, wantSatisfied: false, }, { name: "rpm format constraint", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 1.0.0", RpmFormat), MustGetConstraint("< 0.5.0", RpmFormat), }, }, version: New("1.5.0", RpmFormat), wantValue: ">= 1.0.0 || < 0.5.0", wantString: ">= 1.0.0 || < 0.5.0 (rpm)", wantFormat: RpmFormat, wantSatisfied: true, }, { name: "nil version returns error", constraint: combinedConstraint{ OrOperands: []Constraint{ MustGetConstraint(">= 1.0.0", SemanticFormat), }, }, version: nil, wantValue: ">= 1.0.0", wantString: ">= 1.0.0 (semantic)", wantFormat: SemanticFormat, wantSatisfied: false, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } // test Value() method gotValue := tt.constraint.Value() require.Equal(t, tt.wantValue, gotValue) // test String() method gotString := tt.constraint.String() require.Equal(t, tt.wantString, gotString) // test Format() method gotFormat := tt.constraint.Format() require.Equal(t, tt.wantFormat, gotFormat) // test Satisfied() method gotSatisfied, err := tt.constraint.Satisfied(tt.version) tt.wantErr(t, err) if err != nil { return } require.Equal(t, tt.wantSatisfied, gotSatisfied) }) } } func TestCombinedConstraint_Satisfied_WithErrors(t *testing.T) { tests := []struct { name string constraint combinedConstraint version *Version wantErr require.ErrorAssertionFunc }{ { name: "error from first constraint", constraint: combinedConstraint{ OrOperands: []Constraint{ mockConstraint{value: ">= 1.0.0", format: SemanticFormat, returnErr: true}, mockConstraint{value: "< 2.0.0", format: SemanticFormat, satisfied: true}, }, }, version: New("1.5.0", SemanticFormat), wantErr: require.Error, }, { name: "error from second constraint when first doesn't satisfy", constraint: combinedConstraint{ OrOperands: []Constraint{ mockConstraint{value: ">= 1.0.0", format: SemanticFormat, satisfied: false}, mockConstraint{value: "< 2.0.0", format: SemanticFormat, returnErr: true}, }, }, version: New("1.5.0", SemanticFormat), wantErr: require.Error, }, { name: "no error when first constraint satisfies", constraint: combinedConstraint{ OrOperands: []Constraint{ mockConstraint{value: ">= 1.0.0", format: SemanticFormat, satisfied: true}, mockConstraint{value: "< 2.0.0", format: SemanticFormat, returnErr: true}, }, }, version: New("1.5.0", SemanticFormat), wantErr: require.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := tt.constraint.Satisfied(tt.version) tt.wantErr(t, err) }) } } type mockConstraint struct { value string format Format satisfied bool returnErr bool } func (m mockConstraint) String() string { return m.value + " (" + strings.ToLower(m.format.String()) + ")" } func (m mockConstraint) Value() string { return m.value } func (m mockConstraint) Format() Format { return m.format } func (m mockConstraint) Satisfied(*Version) (bool, error) { if m.returnErr { return false, errors.New("mock constraint error") } return m.satisfied, nil } ================================================ FILE: grype/version/comparator.go ================================================ package version // MissingEpochStrategy defines how missing epochs in package versions are handled // during vulnerability matching. type MissingEpochStrategy string const ( // MissingEpochStrategyZero treats missing epochs as 0 (default, backward compatible) MissingEpochStrategyZero MissingEpochStrategy = "zero" // MissingEpochStrategyAuto assumes missing epoch matches the constraint's epoch MissingEpochStrategyAuto MissingEpochStrategy = "auto" ) // ComparisonConfig contains configuration for version comparison behavior. type ComparisonConfig struct { // MissingEpochStrategy controls how missing epochs in package versions are handled // during vulnerability matching. // // Valid values: // - MissingEpochStrategyZero ("zero"): Treat missing epochs as 0 (default, backward compatible) // - MissingEpochStrategyAuto ("auto"): Assume missing epoch matches the constraint's epoch MissingEpochStrategy MissingEpochStrategy } type Comparator interface { // Compare compares this version to another version. // This returns -1, 0, or 1 if this version is smaller, // equal, or larger than the other version, respectively. Compare(*Version) (int, error) } ================================================ FILE: grype/version/constraint.go ================================================ package version import "fmt" type Constraint interface { fmt.Stringer Value() string Format() Format Satisfied(*Version) (bool, error) } func GetConstraint(constStr string, format Format) (Constraint, error) { var c Constraint var err error switch format { case ApkFormat: c, err = newGenericConstraint(ApkFormat, constStr) case SemanticFormat: c, err = newGenericConstraint(SemanticFormat, constStr) case BitnamiFormat: c, err = newGenericConstraint(BitnamiFormat, constStr) case GemFormat: c, err = newGenericConstraint(GemFormat, constStr) case DebFormat: c, err = newGenericConstraint(DebFormat, constStr) case GolangFormat: c, err = newGenericConstraint(GolangFormat, constStr) case MavenFormat: c, err = newGenericConstraint(MavenFormat, constStr) case RpmFormat: c, err = newGenericConstraint(RpmFormat, constStr) case PythonFormat: c, err = newGenericConstraint(PythonFormat, constStr) case KBFormat: c, err = newKBConstraint(constStr) case PortageFormat: c, err = newGenericConstraint(PortageFormat, constStr) case PacmanFormat: c, err = newGenericConstraint(PacmanFormat, constStr) case JVMFormat: c, err = newGenericConstraint(JVMFormat, constStr) case UnknownFormat: c, err = newFuzzyConstraint(constStr, "unknown") default: return nil, fmt.Errorf("could not find constraint for given format: %s", format) } return c, err } // MustGetConstraint is meant for testing only, do not use within the library func MustGetConstraint(constStr string, format Format) Constraint { c, err := GetConstraint(constStr, format) if err != nil { panic(err) } return c } ================================================ FILE: grype/version/deb_version.go ================================================ package version import ( "strconv" "strings" deb "github.com/knqyf263/go-deb-version" ) var _ Comparator = (*debVersion)(nil) type debVersion struct { obj deb.Version epoch *int // extracted manually since library doesn't export it raw string } func newDebVersion(raw string) (debVersion, error) { ver, err := deb.NewVersion(raw) if err != nil { return debVersion{}, invalidFormatError(DebFormat, raw, err) } // Extract epoch manually for auto strategy support epoch := extractDebEpoch(raw) return debVersion{ obj: ver, epoch: epoch, raw: raw, }, nil } func (v debVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newDebVersion(other.Raw) if err != nil { return 0, err } return v.obj.Compare(o.obj), nil } // CompareWithConfig compares two deb versions using the provided comparison // configuration. The config controls behavior for missing epochs: // - "zero" strategy: missing epochs are treated as 0 // - "auto" strategy: missing epochs in the package version match the constraint's epoch // // Returns: // // -1 if v < other // 0 if v == other // 1 if v > other func (v debVersion) CompareWithConfig(other *Version, cfg ComparisonConfig) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newDebVersion(other.Raw) if err != nil { return 0, err } // Handle auto strategy: if package (v) is missing epoch but constraint (other) has one, // temporarily inject the constraint's epoch into the package version if cfg.MissingEpochStrategy == MissingEpochStrategyAuto { if v.epoch == nil && o.epoch != nil { // Create a temporary version string with the constraint's epoch versionWithEpoch := strconv.Itoa(*o.epoch) + ":" + v.raw vWithEpoch, err := deb.NewVersion(versionWithEpoch) if err != nil { // Fall back to normal comparison if we can't create the modified version return normalizeComparison(v.obj.Compare(o.obj)), nil } return normalizeComparison(vWithEpoch.Compare(o.obj)), nil } } return normalizeComparison(v.obj.Compare(o.obj)), nil } // normalizeComparison normalizes a comparison result to -1, 0, or 1 func normalizeComparison(cmp int) int { if cmp < 0 { return -1 } if cmp > 0 { return 1 } return 0 } // extractDebEpoch extracts the epoch from a Debian version string. // Returns nil if no epoch is present. func extractDebEpoch(raw string) *int { // Debian version format: [epoch:]upstream_version[-debian_revision] // Epoch is optional and separated by a colon colonIndex := strings.Index(raw, ":") if colonIndex == -1 { return nil } epochStr := raw[:colonIndex] epoch, err := strconv.Atoi(epochStr) if err != nil { return nil } return &epoch } ================================================ FILE: grype/version/deb_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDebVersion_Constraint(t *testing.T) { tests := []testCase{ // empty values {version: "2.3.1", constraint: "", satisfied: true}, // compound conditions {version: "2.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: true}, {version: "2.0.0", constraint: "> 1.0.0, <= 2.0.0", satisfied: true}, {version: "2.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.0.0", constraint: ">= 1.0.0, < 2.0.0", satisfied: true}, {version: "1.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "0.9.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, // fixed-in scenarios {version: "2.3.1", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "< 2.0", satisfied: false}, {version: "2.3.1", constraint: "< 2", satisfied: false}, {version: "2.3.1", constraint: "< 2.3", satisfied: false}, {version: "2.3.1", constraint: "< 2.3.1", satisfied: false}, {version: "2.3.1", constraint: "< 2.3.2", satisfied: true}, {version: "2.3.1", constraint: "< 2.4", satisfied: true}, {version: "2.3.1", constraint: "< 3", satisfied: true}, {version: "2.3.1", constraint: "< 3.0", satisfied: true}, {version: "2.3.1", constraint: "< 3.0.0", satisfied: true}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2.0.0", satisfied: false}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2.0", satisfied: false}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2", satisfied: false}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2.3", satisfied: false}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2.3.1", satisfied: false}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2.3.2", satisfied: true}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <2.4", satisfied: true}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <3", satisfied: true}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <3.0", satisfied: true}, {version: "2.3.1-1ubuntu0.14.04.1", constraint: " <3.0.0", satisfied: true}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u151-2.6.11-2ubuntu0.14.04.1", satisfied: false}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u151-2.6.11", satisfied: false}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u151-2.7", satisfied: false}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u151", satisfied: false}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u150", satisfied: false}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u152", satisfied: true}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 7u152-2.6.11-2ubuntu0.14.04.1", satisfied: true}, {version: "7u151-2.6.11-2ubuntu0.14.04.1", constraint: " < 8u1-2.6.11-2ubuntu0.14.04.1", satisfied: true}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43", satisfied: false}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.0", satisfied: false}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.0.2357", satisfied: false}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.0.2357.81", satisfied: false}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.0.2357.81-0ubuntu0.14.04.1.1089", satisfied: false}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.0.2357.82-0ubuntu0.14.04.1.1089", satisfied: true}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.0.2358-0ubuntu0.14.04.1.1089", satisfied: true}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<43.1-0ubuntu0.14.04.1.1089", satisfied: true}, {version: "43.0.2357.81-0ubuntu0.14.04.1.1089", constraint: "<44-0ubuntu0.14.04.1.1089", satisfied: true}, // epoch - both sides have epoch {version: "1:0", constraint: "< 0:1", satisfied: false}, {version: "2:4.19.01-1", constraint: "< 2:4.19.1-1", satisfied: false}, {version: "2:4.19.01-1", constraint: "<= 2:4.19.1-1", satisfied: true}, {version: "0:4.19.1-1", constraint: "< 2:4.19.1-1", satisfied: true}, {version: "11:4.19.0-1", constraint: "< 12:4.19.0-1", satisfied: true}, {version: "13:4.19.0-1", constraint: "< 12:4.19.0-1", satisfied: false}, // epoch - missing epoch treated as 0 (standard dpkg behavior) // This differs from RPM which ignores epochs when only one side has them {version: "1:0", constraint: "< 1", satisfied: false}, // 1:0 vs 0:1 -> epoch 1 > 0, not satisfied {version: "0:0", constraint: "< 0", satisfied: false}, // 0:0 vs 0:0 -> equal, not satisfied {version: "0:0", constraint: "= 0", satisfied: true}, // 0:0 vs 0:0 -> equal {version: "0", constraint: "= 0:0", satisfied: true}, // 0:0 vs 0:0 -> equal {version: "1.0", constraint: "< 2:1.0", satisfied: true}, // 0:1.0 vs 2:1.0 -> epoch 0 < 2, satisfied {version: "1.0", constraint: "<= 2:1.0", satisfied: true}, // 0:1.0 vs 2:1.0 -> epoch 0 < 2, satisfied {version: "1:2", constraint: "< 1", satisfied: false}, // 1:2 vs 0:1 -> epoch 1 > 0, not satisfied {version: "1:2", constraint: "> 1", satisfied: true}, // 1:2 vs 0:1 -> epoch 1 > 0, satisfied {version: "2:4.19.01-1", constraint: "< 4.19.1-1", satisfied: false}, // epoch 2 > 0 {version: "2:4.19.01-1", constraint: "<= 4.19.1-1", satisfied: false}, // epoch 2 > 0 {version: "4.19.01-1", constraint: "< 2:4.19.1-1", satisfied: true}, // epoch 0 < 2 {version: "4.19.0-1", constraint: "< 12:4.19.0-1", satisfied: true}, // epoch 0 < 12 {version: "4.19.0-1", constraint: "<= 12:4.19.0-1", satisfied: true}, // epoch 0 < 12 {version: "3:4.19.0-1", constraint: "< 4.21.0-1", satisfied: false}, // epoch 3 > 0 // real-world debian version formats with epochs {version: "1.5.4-2+deb9u1", constraint: "< 0:1.5.4-2+deb9u1", satisfied: false}, {version: "1.5.4-2+deb9u1", constraint: "<= 0:1.5.4-2+deb9u1", satisfied: true}, {version: "1.5.4-2+deb9u1", constraint: "< 1:1.5.4-2+deb9u1", satisfied: true}, // epoch 0 < 1 {version: "8.3.1-5ubuntu1", constraint: "< 0:8.3.1-5ubuntu2", satisfied: true}, {version: "8.3.1-5ubuntu1.40", constraint: "< 0:8.3.1-5ubuntu1.5", satisfied: false}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, DebFormat) require.NoError(t, err, "unexpected error from GetConstraint: %v", err) test.assertVersionConstraint(t, DebFormat, constraint) }) } } func TestDebVersion_Compare(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3-1", otherVersion: "1.2.3-2", otherFormat: DebFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid deb format", thisVersion: "1.2.3-1", otherVersion: "1.2.3-2", otherFormat: UnknownFormat, expectError: false, }, { name: "with epochs", thisVersion: "1:1.2.3-1", otherVersion: "1:1.2.3-2", otherFormat: DebFormat, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer := New(test.thisVersion, DebFormat) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestDebVersion_Compare_Epochs(t *testing.T) { // Test epoch comparison behavior - this is critical for vulnerability matching // The deb library treats missing epochs as 0, which differs from RPM behavior tests := []struct { name string v1 string v2 string expect string // "less", "equal", "greater" }{ // both have epochs - standard comparison {"epoch 1 vs 0, same version", "1:0", "0:1", "greater"}, {"same epoch, different version", "1:2", "1:1", "greater"}, {"different epochs, same version", "0:4.19.1-1", "2:4.19.1-1", "less"}, {"equal with epochs", "2:1.0-1", "2:1.0-1", "equal"}, // missing epoch treated as 0 (differs from RPM which ignores one-sided epochs) {"epoch 1 vs missing (treated as 0)", "1:0", "1", "greater"}, // 1:0 vs 0:1 -> epoch 1 > 0 {"epoch 2 vs missing", "2:4.19.01-1", "4.19.1-1", "greater"}, // epoch 2 > 0 {"missing vs epoch 2", "4.19.01-1", "2:4.19.1-1", "less"}, // epoch 0 < 2 {"missing vs epoch 12", "4.19.0-1", "12:4.19.0-1", "less"}, // epoch 0 < 12 {"epoch 3 vs missing", "3:4.19.0-1", "4.21.0-1", "greater"}, // epoch 3 > 0 {"missing epoch equal to 0 epoch", "1.0-1", "0:1.0-1", "equal"}, // 0:1.0-1 == 0:1.0-1 {"explicit 0 epoch vs missing", "0:1.0-1", "1.0-1", "equal"}, // 0:1.0-1 == 0:1.0-1 // tilde behavior (not epoch-specific but important for deb) {"tilde sorts before everything", "1.0~rc1-1", "1.0-1", "less"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { v1 := New(test.v1, DebFormat) v2 := New(test.v2, DebFormat) actual, err := v1.Compare(v2) require.NoError(t, err, "unexpected error comparing versions: %s vs %s", test.v1, test.v2) switch test.expect { case "less": assert.Less(t, actual, 0, "expected %s < %s", test.v1, test.v2) case "equal": assert.Equal(t, 0, actual, "expected %s == %s", test.v1, test.v2) case "greater": assert.Greater(t, actual, 0, "expected %s > %s", test.v1, test.v2) } }) } } func TestDebVersion_CompareWithConfig(t *testing.T) { tests := []struct { name string version string other string strategy MissingEpochStrategy want int // -1, 0, or 1 }{ { name: "package has epoch, no behavior change with auto", version: "1:2.0.0", other: "1:1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // 1:2.0.0 > 1:1.5.0 }, { name: "package has epoch, no behavior change with zero", version: "1:2.0.0", other: "1:1.5.0", strategy: "zero", want: 1, // 1:2.0.0 > 1:1.5.0 }, { name: "package missing epoch, constraint has epoch, auto strategy - no match", version: "2.0.0", other: "1:1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // Treated as 1:2.0.0 > 1:1.5.0 }, { name: "package missing epoch, constraint has epoch, zero strategy - match", version: "2.0.0", other: "1:1.5.0", strategy: "zero", want: -1, // Treated as 0:2.0.0 < 1:1.5.0 }, { name: "both missing epoch, auto strategy", version: "2.0.0", other: "1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // 2.0.0 > 1.5.0 }, { name: "both missing epoch, zero strategy", version: "2.0.0", other: "1.5.0", strategy: "zero", want: 1, // 2.0.0 > 1.5.0 }, { name: "constraint missing epoch, package has epoch", version: "1:2.0.0-1", other: "1.5.0-1", strategy: MissingEpochStrategyAuto, want: 1, // 1:2.0.0 > 0:1.5.0 (constraint gets epoch 0) }, { name: "auto strategy, package less than constraint", version: "1.0.0", other: "1:1.5.0", strategy: MissingEpochStrategyAuto, want: -1, // Treated as 1:1.0.0 < 1:1.5.0 }, { name: "auto strategy, different epochs on constraints", version: "1.2.0", other: "2:1.5.0", strategy: MissingEpochStrategyAuto, want: -1, // Treated as 2:1.2.0 < 2:1.5.0 }, { name: "zero strategy, package version newer but lower epoch", version: "3.0.0", other: "1:1.0.0", strategy: "zero", want: -1, // 0:3.0.0 < 1:1.0.0 because epoch 0 < 1 }, { name: "auto strategy, equal versions different missing epochs", version: "1.2.3-1", other: "1:1.2.3-1", strategy: MissingEpochStrategyAuto, want: 0, // Treated as 1:1.2.3 == 1:1.2.3 }, { name: "zero strategy, equal versions different missing epochs", version: "1.2.3-1", other: "1:1.2.3-1", strategy: "zero", want: -1, // 0:1.2.3 < 1:1.2.3 }, { name: "auto strategy, large epoch difference", version: "1.0.0", other: "999:0.5.0", strategy: MissingEpochStrategyAuto, want: 1, // Treated as 999:1.0.0 > 999:0.5.0 }, { name: "zero strategy, large epoch difference", version: "1.0.0", other: "999:0.5.0", strategy: "zero", want: -1, // 0:1.0.0 < 999:0.5.0 }, { name: "both have epochs, strategy should not matter", version: "2:1.5.0-1", other: "1:2.0.0-1", strategy: MissingEpochStrategyAuto, want: 1, // 2:1.5.0 > 1:2.0.0 (epoch takes precedence) }, { name: "both have same epoch, strategy should not matter", version: "3:2.0.0-1", other: "3:1.5.0-1", strategy: "zero", want: 1, // 3:2.0.0 > 3:1.5.0 }, { name: "debian revision comparison with auto", version: "1.0-1ubuntu1", other: "1:1.0-1ubuntu1", strategy: MissingEpochStrategyAuto, want: 0, // Treated as 1:1.0-1ubuntu1 == 1:1.0-1ubuntu1 }, { name: "empty strategy uses default behavior (zero-like)", version: "2.0.0", other: "1:1.5.0", strategy: "", want: -1, // Should behave like zero strategy when empty }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v1, err := newDebVersion(tt.version) require.NoError(t, err) v2 := &Version{ Format: DebFormat, Raw: tt.other, } cfg := ComparisonConfig{ MissingEpochStrategy: tt.strategy, } result, err := v1.CompareWithConfig(v2, cfg) require.NoError(t, err) assert.Equal(t, tt.want, result, "comparing %s vs %s with strategy %s", tt.version, tt.other, tt.strategy) }) } } func TestDebVersion_CompareWithConfig_ErrorCases(t *testing.T) { tests := []struct { name string version string other *Version strategy MissingEpochStrategy wantErr bool }{ { name: "nil other version", version: "1.0.0", other: nil, strategy: MissingEpochStrategyAuto, wantErr: true, }, { name: "invalid other version format", version: "1.0.0", other: &Version{Format: DebFormat, Raw: "not-a-valid-debian-version!@#$%"}, strategy: MissingEpochStrategyAuto, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v1, err := newDebVersion(tt.version) require.NoError(t, err) cfg := ComparisonConfig{ MissingEpochStrategy: tt.strategy, } _, err = v1.CompareWithConfig(tt.other, cfg) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestDebVersion_CompareWithConfig_ConsistencyWithCompare(t *testing.T) { // Test that when both versions have epochs, CompareWithConfig gives same result as Compare tests := []struct { v1 string v2 string }{ {"1:2.0.0-1", "1:1.5.0-1"}, {"2:1.0.0-1ubuntu1", "1:2.0.0-1ubuntu1"}, {"0:1.2.3-1", "0:1.2.3-1"}, {"5:1.0.0-1", "5:1.0.0-2"}, } for _, tt := range tests { t.Run(tt.v1+"_vs_"+tt.v2, func(t *testing.T) { v1, _ := newDebVersion(tt.v1) v2 := &Version{Format: DebFormat, Raw: tt.v2} // Test with both strategies for _, strategy := range []MissingEpochStrategy{MissingEpochStrategyZero, MissingEpochStrategyAuto} { cfg := ComparisonConfig{MissingEpochStrategy: strategy} resultWithConfig, err1 := v1.CompareWithConfig(v2, cfg) require.NoError(t, err1) resultNormal, err2 := v1.Compare(v2) require.NoError(t, err2) assert.Equal(t, resultNormal, resultWithConfig, "when both versions have epochs, CompareWithConfig should match Compare (strategy: %s)", strategy) } }) } } func TestExtractDebEpoch(t *testing.T) { tests := []struct { name string version string wantNil bool expected int }{ { name: "no epoch", version: "1.2.3-1", wantNil: true, expected: 0, }, { name: "epoch 0", version: "0:1.2.3-1", wantNil: false, expected: 0, }, { name: "epoch 1", version: "1:1.2.3-1", wantNil: false, expected: 1, }, { name: "large epoch", version: "999:1.0.0", wantNil: false, expected: 999, }, { name: "epoch with complex version", version: "5:2.0.0-1ubuntu0.14.04.1", wantNil: false, expected: 5, }, { name: "multiple colons - only first is epoch", version: "1:2:3.4.5", wantNil: false, expected: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractDebEpoch(tt.version) if tt.wantNil { assert.Nil(t, result, "expected nil epoch for version %s", tt.version) } else { require.NotNil(t, result, "expected non-nil epoch for version %s", tt.version) assert.Equal(t, tt.expected, *result, "epoch value mismatch for version %s", tt.version) } }) } } ================================================ FILE: grype/version/deprecated.go ================================================ package version // NewVersion creates a new Version instance with the provided raw version string and format. // // Deprecated: NewVersion is deprecated, use New instead. func NewVersion(raw string, format Format) *Version { return New(raw, format) } ================================================ FILE: grype/version/error.go ================================================ package version import ( "errors" "fmt" ) // ErrUnsupportedVersion is returned when a version string cannot be parsed because the value is known // to cause issues or is otherwise problematic (e.g. golang "devel" version). var ErrUnsupportedVersion = fmt.Errorf("unsupported version value") // ErrNoVersionProvided is returned when a version is attempted to be compared, but no other version is provided to compare against. var ErrNoVersionProvided = errors.New("no version provided for comparison") // UnsupportedComparisonError represents an error when a format doesn't match the expected format type UnsupportedComparisonError struct { Left Format Right *Version } // newUnsupportedFormatError creates a new UnsupportedComparisonError func newUnsupportedFormatError(left Format, right *Version) *UnsupportedComparisonError { return &UnsupportedComparisonError{ Left: left, Right: right, } } func (e *UnsupportedComparisonError) Error() string { return fmt.Sprintf("(%s) unsupported version comparison: value=%q format=%q", e.Left, e.Right.Raw, e.Right.Format) } func (e *UnsupportedComparisonError) Is(target error) bool { var t *UnsupportedComparisonError ok := errors.As(target, &t) if !ok { return false } return (t.Left == UnknownFormat || t.Left == e.Left) && (t.Right.Format == UnknownFormat || t.Right == e.Right) } func invalidFormatError(format Format, raw string, err error) error { return fmt.Errorf("invalid %s version from '%s': %w", format.String(), raw, err) } // NonFatalConstraintError should be used any time an unexpected but recoverable condition is encountered while // checking version constraint satisfaction. The error should get returned by any implementer of the Constraint // interface. If returned by the Satisfied method on the Constraint interface, this error will be caught and // logged as a warning in the FindMatchesByPackageDistro function in grype/matcher/common/distro_matchers.go type NonFatalConstraintError struct { constraint Constraint version *Version message string } func (e NonFatalConstraintError) Error() string { return fmt.Sprintf("matching raw constraint %s against version %s caused a non-fatal error: %s", e.constraint, e.version, e.message) } ================================================ FILE: grype/version/format.go ================================================ package version import ( "strings" "github.com/anchore/packageurl-go" ) const ( UnknownFormat Format = iota SemanticFormat ApkFormat DebFormat MavenFormat RpmFormat PythonFormat KBFormat GemFormat PortageFormat GolangFormat JVMFormat BitnamiFormat PacmanFormat ) type Format int var formatStr = []string{ "Unknown", "Semantic", "Apk", "Deb", "Maven", "RPM", "Python", "KB", "Gem", "Portage", "Go", "JVM", "Bitnami", "Pacman", } var Formats = []Format{ SemanticFormat, ApkFormat, DebFormat, MavenFormat, RpmFormat, PythonFormat, KBFormat, GemFormat, PortageFormat, GolangFormat, JVMFormat, BitnamiFormat, PacmanFormat, } func ParseFormat(userStr string) Format { switch strings.ToLower(userStr) { // sever includes known ecosystem types that use semver or a very semver-like schemes case strings.ToLower(SemanticFormat.String()), "semver", packageurl.TypeNPM, packageurl.TypeNuget, packageurl.TypeComposer, packageurl.TypeHex, packageurl.TypePub, packageurl.TypeSwift, packageurl.TypeConan, packageurl.TypeCocoapods, packageurl.TypeHackage: return SemanticFormat case strings.ToLower(ApkFormat.String()), "apk": return ApkFormat case strings.ToLower(BitnamiFormat.String()), "bitnami": return BitnamiFormat case strings.ToLower(DebFormat.String()), "dpkg", packageurl.TypeDebian: return DebFormat case strings.ToLower(GolangFormat.String()), "go", packageurl.TypeGolang: return GolangFormat case strings.ToLower(MavenFormat.String()), "maven": return MavenFormat case strings.ToLower(RpmFormat.String()), "rpm": return RpmFormat case strings.ToLower(PythonFormat.String()), "python", packageurl.TypePyPi, "pep440": return PythonFormat case strings.ToLower(KBFormat.String()), "kb": return KBFormat case strings.ToLower(GemFormat.String()), "gem": return GemFormat case strings.ToLower(PortageFormat.String()), "portage": return PortageFormat case strings.ToLower(JVMFormat.String()), "jvm", "jre", "jdk", "openjdk", "jep223": return JVMFormat case strings.ToLower(PacmanFormat.String()), "pacman": return PacmanFormat } return UnknownFormat } func (f Format) String() string { if int(f) >= len(formatStr) || f < 0 { return formatStr[0] } return formatStr[f] } ================================================ FILE: grype/version/format_test.go ================================================ package version import ( "fmt" "testing" ) func TestParseFormat(t *testing.T) { tests := []struct { input string format Format }{ // SemanticFormat cases { input: "semantic", format: SemanticFormat, }, { input: "semver", format: SemanticFormat, }, { input: "npm", format: SemanticFormat, }, { input: "nuget", format: SemanticFormat, }, { input: "composer", format: SemanticFormat, }, { input: "hex", format: SemanticFormat, }, { input: "pub", format: SemanticFormat, }, { input: "swift", format: SemanticFormat, }, { input: "conan", format: SemanticFormat, }, { input: "cocoapods", format: SemanticFormat, }, { input: "hackage", format: SemanticFormat, }, // ApkFormat cases { input: "apk", format: ApkFormat, }, // BitnamiFormat cases { input: "bitnami", format: BitnamiFormat, }, // DebFormat cases { input: "deb", format: DebFormat, }, { input: "dpkg", format: DebFormat, }, // GolangFormat cases { input: "golang", format: GolangFormat, }, { input: "go", format: GolangFormat, }, // MavenFormat cases { input: "maven", format: MavenFormat, }, // RpmFormat cases { input: "rpm", format: RpmFormat, }, // PythonFormat cases { input: "python", format: PythonFormat, }, { input: "pypi", format: PythonFormat, }, { input: "pep440", format: PythonFormat, }, // KBFormat cases { input: "kb", format: KBFormat, }, // GemFormat cases { input: "gem", format: GemFormat, }, // PortageFormat cases { input: "portage", format: PortageFormat, }, // JVMFormat cases { input: "jvm", format: JVMFormat, }, { input: "jre", format: JVMFormat, }, { input: "jdk", format: JVMFormat, }, { input: "openjdk", format: JVMFormat, }, { input: "jep223", format: JVMFormat, }, // PacmanFormat cases { input: "pacman", format: PacmanFormat, }, // UnknownFormat case { input: "unknown", format: UnknownFormat, }, } for _, test := range tests { name := fmt.Sprintf("'%s'->format[%s]", test.input, test.format) t.Run(name, func(t *testing.T) { actual := ParseFormat(test.input) if actual != test.format { t.Errorf("mismatched user string -> format mapping, pkgType='%s': '%s'!='%s'", test.input, test.format, actual) } }) } } ================================================ FILE: grype/version/fuzzy_constraint.go ================================================ package version import ( "fmt" "regexp" "strconv" "strings" "unicode" hashiVer "github.com/anchore/go-version" ) // derived from https://semver.org/, but additionally matches: // - partial versions (e.g. "2.0") // - optional prefix "v" (e.g. "v1.0.0") var pseudoSemverPattern = regexp.MustCompile(`^v?(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?(?:(-|alpha|beta|rc)((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) type fuzzyConstraint struct { RawPhrase string PhraseHint string SemanticConstraint *hashiVer.Constraints Constraints simpleRangeExpression } func newFuzzyConstraint(phrase, hint string) (fuzzyConstraint, error) { if phrase == "" { // an empty constraint is always satisfied return fuzzyConstraint{ RawPhrase: phrase, PhraseHint: hint, }, nil } constraints, err := parseRangeExpression(phrase) if err != nil { return fuzzyConstraint{}, fmt.Errorf("could not create fuzzy constraint: %+v", err) } var semverConstraint *hashiVer.Constraints // check all version unit phrases to see if this is a valid semver constraint valid := true check: for _, units := range constraints.Units { for _, unit := range units { if !pseudoSemverPattern.MatchString(unit.Version) { valid = false break check } } } if value, err := hashiVer.NewConstraint(phrase); err == nil && valid { semverConstraint = &value } return fuzzyConstraint{ RawPhrase: phrase, PhraseHint: hint, Constraints: constraints, SemanticConstraint: semverConstraint, }, nil } func (f fuzzyConstraint) Satisfied(verObj *Version) (bool, error) { if f.RawPhrase == "" && verObj != nil { // an empty constraint is always satisfied return true, nil } else if verObj == nil { if f.RawPhrase != "" { // a non-empty constraint with no version given should always fail return false, nil } return true, nil } version := verObj.Raw // rebuild temp constraint based off of ver obj if verObj.Format != UnknownFormat { newConstraint, err := GetConstraint(f.RawPhrase, verObj.Format) // check if constraint is not fuzzyConstraint _, ok := newConstraint.(fuzzyConstraint) if err == nil && !ok { satisfied, err := newConstraint.Satisfied(verObj) if err == nil { return satisfied, nil } } } // attempt semver first, then fallback to fuzzy part matching... if f.SemanticConstraint != nil { if pseudoSemverPattern.MatchString(version) { // we're stricter about accepting looser semver rules here since we have no context about // the true format of the version, thus we want to reduce the change of false negatives if semver, err := newSemanticVersion(version, true); err == nil { return f.SemanticConstraint.Check(semver.obj), nil } } } // semver didn't work, use fuzzy part matching instead... return f.Constraints.satisfied(UnknownFormat, verObj) } func (f fuzzyConstraint) Format() Format { return UnknownFormat } func (f fuzzyConstraint) String() string { if f.RawPhrase == "" { return "none (unknown)" } if f.PhraseHint != "" { return fmt.Sprintf("%s (%s)", f.RawPhrase, f.PhraseHint) } return fmt.Sprintf("%s (unknown)", f.RawPhrase) } func (f fuzzyConstraint) Value() string { return f.RawPhrase } // Note: the below code is from https://github.com/facebookincubator/nvdtools/blob/688794c4d3a41929eeca89304e198578d4595d53/cvefeed/nvd/smartvercmp.go (apache V2) // I'd prefer to import this functionality instead of copying it, however, these functions are not exported from the package // fuzzyVersionComparison compares stringified versions of software. // It tries to do the right thing for any unspecified version type, // assuming v1 and v2 have the same version convention. // It will return meaningful result for "95SE" vs "98SP1" or for "16.3.2" vs. "3.7.0", // but not for "2000" vs "11.7". // Returns -1 if v1 < v2, 1 if v1 > v2 and 0 if v1 == v2. func fuzzyVersionComparison(v1, v2 string) int { v1 = stripLeadingV(v1) v2 = stripLeadingV(v2) for s1, s2 := v1, v2; len(s1) > 0 && len(s2) > 0; { num1, cmpTo1, skip1 := parseVersionParts(s1) num2, cmpTo2, skip2 := parseVersionParts(s2) ns1 := s1[:cmpTo1] ns2 := s2[:cmpTo2] diff := num1 - num2 switch { case diff > 0: // ns1 has longer numeric part ns2 = leftPad(ns2, diff) case diff < 0: // ns2 has longer numeric part ns1 = leftPad(ns1, -diff) } // Check if both parts look like they have patch numbers (e.g., "p9" vs "p15") if hasPatchNumber(ns1) && hasPatchNumber(ns2) { if cmp := comparePatchNumbers(ns1, ns2); cmp != 0 { return cmp } } else if cmp := strings.Compare(ns1, ns2); cmp != 0 { return cmp } s1 = s1[skip1:] s2 = s2[skip2:] } // everything is equal so far, the longest wins if len(v1) > len(v2) { return 1 } if len(v2) > len(v1) { return -1 } return 0 } // parseVersionParts returns the length of consecutive run of digits in the beginning of the string, // the last non-separator character (which should be compared), and index at which the version part (major, minor etc.) ends, // i.e. the position of the dot or end of the line. // E.g. parseVersionParts("11.b4.16-New_Year_Edition") will return (2, 3, 4) func parseVersionParts(v string) (int, int, int) { var num int for num = 0; num < len(v); num++ { if v[num] < '0' || v[num] > '9' { break } } if num == len(v) { return num, num, num } // Any punctuation separates the parts. skip := strings.IndexFunc(v, func(b rune) bool { // !"#$%&'()*+,-./ are dec 33 to 47, :;<=>?@ are dec 58 to 64, [\]^_` are dec 91 to 96 and {|}~ are dec 123 to 126. // So, punctuation is in dec 33-126 range except 48-57, 65-90 and 97-122 gaps. // This inverse logic allows for early short-circuiting for most of the chars and shaves ~20ns in benchmarks. // linters might yell about De Morgan's law here - we ignore them in this case //nolint:staticcheck return b >= '!' && b <= '~' && !(b > '/' && b < ':' || b > '@' && b < '[' || b > '`' && b < '{') }) if skip == -1 { return num, len(v), len(v) } return num, skip, skip + 1 } // leftPad pads s with n '0's func leftPad(s string, n int) string { var sb strings.Builder for i := 0; i < n; i++ { sb.WriteByte('0') } sb.WriteString(s) return sb.String() } func stripLeadingV(ver string) string { return strings.TrimPrefix(ver, "v") } // hasPatchNumber returns true if the version segment looks like it has a patch number // e.g., "p9", "p15", "rc1", "alpha2", "8p9", "8p15" func hasPatchNumber(segment string) bool { for i, r := range segment { if unicode.IsLetter(r) && i < len(segment)-1 { next := rune(segment[i+1]) if unicode.IsDigit(next) { return true } } } return false } // comparePatchNumbers compares version segments with patch numbers numerically // e.g., "p9" < "p15", "rc1" < "rc10", "8p9" < "8p15" func comparePatchNumbers(left, right string) int { findLetterDigitBoundary := func(s string) int { for i, r := range s { if unicode.IsLetter(r) && i < len(s)-1 && unicode.IsDigit(rune(s[i+1])) { return i + 1 } } return -1 } leftPos := findLetterDigitBoundary(left) rightPos := findLetterDigitBoundary(right) if leftPos > 0 && rightPos > 0 { leftPrefix, leftNumStr := left[:leftPos], left[leftPos:] rightPrefix, rightNumStr := right[:rightPos], right[rightPos:] if cmp := strings.Compare(leftPrefix, rightPrefix); cmp != 0 { return cmp } if leftNum, err1 := strconv.Atoi(leftNumStr); err1 == nil { if rightNum, err2 := strconv.Atoi(rightNumStr); err2 == nil { if leftNum < rightNum { return -1 } else if leftNum > rightNum { return 1 } } } } return strings.Compare(left, right) } ================================================ FILE: grype/version/fuzzy_constraint_test.go ================================================ package version import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestFuzzyVersionComparison(t *testing.T) { cases := []struct { v1, v2 string ret int }{ // Python PEP440 craziness {"1.5+1", "1.5+1.git.abc123de", -1}, {"1.0.0-post1", "1.0.0-post2", -1}, {"1.0.0", "1.0.0-post1", -1}, {"1.0.0-dev1", "1.0.0-post1", -1}, {"1.0.0-dev2", "1.0.0-post1", -1}, {"1.0.0", "1.0.0-dev1", -1}, {"5", "8", -1}, {"15", "3", 1}, {"4a", "4c", -1}, {"1.0", "1.0", 0}, {"1.0.1", "1.0", 1}, {"1.0.14", "1.0.4", 1}, {"95SE", "98SP1", -1}, {"98SE", "98SP1", -1}, {"98SP1", "98SP3", -1}, {"16.0.0", "3.2.7", 1}, {"10.23", "10.21", 1}, {"64.0", "3.6.24", 1}, {"5-1.15", "5-1.16", -1}, {"5-1.15.2", "5-1.16", -1}, {"5-appl_1.16.1", "5-1.0.1", -1}, // this is wrong, but seems to be impossible to account for {"5-1.16", "5_1.0.6", 1}, {"5-6", "5-16", -1}, {"5a1", "5a2", -1}, {"5a1", "6a1", -1}, {"5-a1", "5a1", -1}, // meh, kind of makes sense {"5-a1", "5.a1", 0}, {"1.4", "1.02", 1}, {"5.0", "08.0", -1}, {"10.0", "1.0", 1}, {"10.0", "1.000", 1}, {"10.0", "1.000.0.1", 1}, {"1.0.4", "1.0.4+metadata", -1}, // this is also somewhat wrong, however, there is a semver parser that can handle this case (which should be leveraged when possible) {"1.3.2-r0", "1.3.3-r0", -1}, // regression: regression for https://github.com/anchore/go-version/pull/2 // Java JRE/JDK versioning prior to the implementing https://openjdk.org/jeps/223 for >= version 9 {"1.8.0_456", "1.8.0", 1}, {"1.8.0_456", "1.8.0_234", 1}, {"1.8.0_456", "1.8.0_457", -1}, {"1.8.0_456-b1", "1.8.0_456-b2", -1}, {"1.8.0_456", "1.8.0_456-b1", -1}, // Also check the semver equivalents of pre java version 9 work as expected: {"8.0.456", "8.0", 1}, {"8.0.456", "8.0.234", 1}, {"8.0.456", "8.0.457", -1}, {"8.0.456+1", "8.0.456+2", -1}, {"8.0.456", "8.0.456+1", -1}, // Test case for fuzzy version comparison bug with patch numbers // This should pass: 4.2.8p9 < 4.2.8p15 (p9 comes before p15 numerically) // But currently fails due to lexicographic fallback where "p9" > "p15" (string comparison) {"4.2.8p9", "4.2.8p15", -1}, // douple check openssl's unusual versioning // 1.0.2k is an earlier patch release than 1.0.2l {"1.0.2k", "1.0.2l", -1}, // 1.1.1w is a later patch on 1.1.1 {"1.1.1", "1.1.1w", -1}, } for _, c := range cases { t.Run(fmt.Sprintf("%q vs %q", c.v1, c.v2), func(t *testing.T) { if ret := fuzzyVersionComparison(c.v1, c.v2); ret != c.ret { t.Fatalf("expected %d, got %d", c.ret, ret) } }) } } func TestFuzzyVersion_Constraint(t *testing.T) { tests := []testCase{ { name: "empty constraint", version: "2.3.1", constraint: "", satisfied: true, }, { name: "version range within", constraint: ">1.0, <2.0", version: "1.2+beta-3", satisfied: true, }, { name: "version within compound range", constraint: ">1.0, <2.0 || > 3.0", version: "3.2+beta-3", satisfied: true, }, { name: "version within compound range (2)", constraint: ">1.0, <2.0 || > 3.0", version: "1.2+beta-3", satisfied: true, }, { name: "version not within compound range", constraint: ">1.0, <2.0 || > 3.0", version: "2.2+beta-3", satisfied: false, }, { name: "version range within (prerelease)", constraint: ">1.0, <2.0", version: "1.2.0-beta-prerelease", satisfied: true, }, { name: "version range within (prerelease)", constraint: ">=1.0, <2.0", version: "1.0.0-beta-prerelease", satisfied: false, }, { name: "version range outside (right)", constraint: ">1.0, <2.0", version: "2.1-beta-3", satisfied: false, }, { name: "version range outside (left)", constraint: ">1.0, <2.0", version: "0.9-beta-2", satisfied: false, }, { name: "version range within (excluding left, prerelease)", constraint: ">=1.0, <2.0", version: "1.0-beta-3", satisfied: false, }, { name: "version range within (including left)", constraint: ">=1.1, <2.0", version: "1.1", satisfied: true, }, { name: "version range within (excluding right, 1)", constraint: ">1.0, <=2.0", version: "2.0-beta-3", satisfied: true, }, { name: "version range within (excluding right, 2)", constraint: ">1.0, <2.0", version: "2.0-beta-3", satisfied: true, }, { name: "version range within (including right)", constraint: ">1.0, <=2.0", version: "2.0", satisfied: true, }, { name: "version range within (including right, longer version [valid semver, bad fuzzy])", constraint: ">1.0, <=2.0", version: "2.0.0", satisfied: true, }, { name: "version range not within range (prefix)", constraint: ">1.0, <2.0", version: "5-1.2+beta-3", satisfied: false, }, { name: "odd major prefix wide constraint range", constraint: ">4, <6", version: "5-1.2+beta-3", satisfied: true, }, { name: "odd major prefix narrow constraint", constraint: ">5-1.15", version: "5-1.16", satisfied: true, }, { name: "odd major prefix narrow constraint range", constraint: ">5-1.15, <=5-1.16", version: "5-1.16", satisfied: true, }, { name: "odd major prefix narrow constraint range (excluding)", constraint: ">4, <5-1.16", version: "5-1.16", satisfied: false, }, { name: "bad semver (eq)", version: "5a2", // with the hashicorp lib, without the strict check, this is interpreted as 5.0.0-alpha.2 constraint: "=5a2", satisfied: true, }, { name: "bad semver (gt)", version: "5a2", constraint: ">5a1", satisfied: true, }, { name: "bad semver (lt)", version: "5a2", constraint: "<6a1", satisfied: true, }, { name: "bad semver (lte)", version: "5a2", constraint: "<=5a2", satisfied: true, }, { name: "bad semver (gte)", version: "5a2", constraint: ">=5a2", satisfied: true, }, { name: "bad semver (lt boundary)", version: "5a2", constraint: "<5a2", satisfied: false, }, // regression for https://github.com/anchore/go-version/pull/2 { name: "indirect package match", version: "1.3.2-r0", constraint: "<= 1.3.3-r0", satisfied: true, }, { name: "indirect package no match", version: "1.3.4-r0", constraint: "<= 1.3.3-r0", satisfied: false, }, { name: "vulndb fuzzy constraint single quoted", version: "4.5.2", constraint: "'4.5.1' || '4.5.2'", satisfied: true, }, { name: "vulndb fuzzy constraint double quoted", version: "4.5.2", constraint: "\"4.5.1\" || \"4.5.2\"", satisfied: true, }, { name: "strip unbalanced v from left side <", version: "v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible", constraint: "< 1.5", satisfied: false, }, { name: "strip unbalanced v from left side >", version: "v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible", constraint: "> 1.5", satisfied: true, }, { name: "strip unbalanced v from right side <", version: "17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible", constraint: "< v1.5", satisfied: false, }, { name: "strip unbalanced v from right side >", version: "17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible", constraint: "> v1.5", satisfied: true, }, { name: "rc candidates with no '-' can match semver pattern", version: "1.20rc1", constraint: " = 1.20.0-rc1", satisfied: true, }, { name: "candidates ahead of alpha", version: "3.11.0", constraint: "> 3.11.0-alpha1", satisfied: true, }, { name: "candidates ahead of beta", version: "3.11.0", constraint: "> 3.11.0-beta1", satisfied: true, }, { name: "candidates ahead of same alpha versions", version: "3.11.0-alpha5", constraint: "> 3.11.0-alpha1", satisfied: true, }, { name: "candidates are placed correctly between alpha and release", version: "3.11.0-beta5", constraint: "3.11.0 || = 3.11.0-alpha1", satisfied: false, }, { name: "candidates with letter suffix are alphabetically greater than their versions", version: "1.0.2a", constraint: " < 1.0.2w", satisfied: true, }, { name: "candidates with multiple letter suffix are alphabetically greater than their versions", version: "1.0.2zg", constraint: " < 1.0.2zh", satisfied: true, }, { name: "candidates with pre suffix are sorted numerically", version: "1.0.2pre1", constraint: " < 1.0.2pre2", satisfied: true, }, { name: "candidates with letter suffix and r0 are alphabetically greater than their versions", version: "1.0.2k-r0", constraint: " < 1.0.2l-r0", satisfied: true, }, { name: "openssl version with letter suffix and r0 are alphabetically greater than their versions", version: "1.0.2k-r0", // the lib is saying the there is a prerelese starting at "k-r0" constraint: ">= 1.0.2", satisfied: true, }, { name: "openssl versions with letter suffix and r0 are alphabetically greater than their versions and compared equally to other lettered versions", version: "1.0.2k-r0", constraint: ">= 1.0.2, < 1.0.2m", satisfied: true, }, { name: "openssl pre2 is still considered less than release", version: "1.1.1-pre2", constraint: "> 1.1.1-pre1, < 1.1.1", satisfied: true, }, { name: "major version releases are less than their subsequent patch releases with letter suffixes", version: "1.1.1", constraint: "> 1.1.1-a", satisfied: true, }, { name: "go pseudoversion vulnerable: version is less, want less", version: "0.0.0-20230716120725-531d2d74bc12", constraint: "<0.0.0-20230922105210-14b16010c2ee", satisfied: true, }, { name: "go pseudoversion not vulnerable: same version but constraint is less", version: "0.0.0-20230922105210-14b16010c2ee", constraint: "<0.0.0-20230922105210-14b16010c2ee", satisfied: false, }, { name: "go pseudoversion not vulnerable: greater version", version: "0.0.0-20230922112808-5421fefb8386", constraint: "<0.0.0-20230922105210-14b16010c2ee", satisfied: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { constraint, err := GetConstraint(test.constraint, UnknownFormat) assert.NoError(t, err, "unexpected error from newFuzzyConstraint: %v", err) test.assertVersionConstraint(t, UnknownFormat, constraint) }) } } func TestPseudoSemverPattern(t *testing.T) { tests := []struct { name string version string valid bool }{ {name: "rc candidates are valid semver", version: "1.2.3-rc1", valid: true}, {name: "rc candidates with no dash are valid semver", version: "1.2.3rc1", valid: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert.Equal(t, test.valid, pseudoSemverPattern.MatchString(test.version)) }) } } ================================================ FILE: grype/version/fuzzy_version.go ================================================ package version var _ Comparator = (*fuzzyVersion)(nil) type fuzzyVersion struct { semVer *semanticVersion raw string } //nolint:unparam func newFuzzyVersion(raw string) (fuzzyVersion, error) { return fuzzyVersion{ semVer: newFuzzySemver(raw), raw: raw, }, nil } func (v fuzzyVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } semver := newFuzzySemver(other.Raw) if semver != nil && v.semVer != nil && v.semVer.obj != nil && semver.obj != nil { return v.semVer.obj.Compare(semver.obj), nil } // one or both are no semver compliant, use fuzzy comparison return fuzzyVersionComparison(v.raw, other.Raw), nil } func newFuzzySemver(raw string) *semanticVersion { // we need to be a little more strict here than the hashicorp lib, but not as strict as the semver spec. // a good example of this is being able to reason about openssl versions like "1.0.2k" or "1.0.2l" which are // not semver compliant, but we still want to be able to compare them. But the hashicorp lib will not parse // the postfix letter as a prerelease version, which is wrong. In these cases we want a true fuzzy version // comparison. if pseudoSemverPattern.MatchString(raw) { candidate, err := newSemanticVersion(raw, false) if err == nil { return &candidate } } return nil } ================================================ FILE: grype/version/fuzzy_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFuzzyVersion_Compare(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "fuzzy comparison with semantic version", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: SemanticFormat, expectError: false, }, { name: "fuzzy comparison with unknown format", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: UnknownFormat, expectError: false, }, { name: "fuzzy comparison with different format", thisVersion: "1.2.3", otherVersion: "1.2.3-r4", otherFormat: ApkFormat, expectError: false, }, { name: "fuzzy comparison with non-semantic string", thisVersion: "1.2.3", otherVersion: "abc123", otherFormat: UnknownFormat, expectError: false, }, { name: "fuzzy comparison with empty strings", thisVersion: "1.2.3", otherVersion: "", otherFormat: UnknownFormat, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer := New(test.thisVersion, UnknownFormat) // explicitly use the fuzzy version format otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestFuzzyVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(tb testing.TB) (*Version, *Version) expectError require.ErrorAssertionFunc errorSubstring string wantComparison int }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3", UnknownFormat) return thisVer, nil }, expectError: require.Error, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.expectError == nil { test.expectError = require.NoError } thisVer, otherVer := test.setupFunc(t) n, err := thisVer.Compare(otherVer) test.expectError(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } if err != nil { return } assert.Equal(t, test.wantComparison, n, "Expected comparison result to be %d", test.wantComparison) }) } } func TestFuzzyVersion_Compare_NilScenarios(t *testing.T) { tests := []struct { name string setupFunc func(tb testing.TB) (fuzzyVersion, *Version) expectFallback bool // expect fuzzy comparison fallback }{ { name: "both v.semVer and other semver are nil", setupFunc: func(t testing.TB) (fuzzyVersion, *Version) { // create fuzzyVersion with nil semVer fv := fuzzyVersion{ semVer: nil, raw: "abc123", } otherVer := New("def456", UnknownFormat) return fv, otherVer }, expectFallback: true, }, { name: "v.semVer is nil, other semver is not nil", setupFunc: func(t testing.TB) (fuzzyVersion, *Version) { // create fuzzyVersion with nil semVer fv := fuzzyVersion{ semVer: nil, raw: "abc123", } otherVer := New("1.2.3", UnknownFormat) return fv, otherVer }, expectFallback: true, }, { name: "v.semVer is not nil but v.semVer.obj is nil", setupFunc: func(t testing.TB) (fuzzyVersion, *Version) { // create fuzzyVersion with semVer that has nil obj fv := fuzzyVersion{ semVer: &semanticVersion{obj: nil}, raw: "abc123", } otherVer := New("1.2.3", UnknownFormat) return fv, otherVer }, expectFallback: true, }, { name: "v.semVer is valid but other semver is nil", setupFunc: func(t testing.TB) (fuzzyVersion, *Version) { // create fuzzyVersion with valid semVer semVer, err := newSemanticVersion("1.2.3", false) require.NoError(t, err) fv := fuzzyVersion{ semVer: &semVer, raw: "1.2.3", } // create other version that will result in nil semver from newFuzzySemver otherVer := New("abc123", UnknownFormat) return fv, otherVer }, expectFallback: true, }, { name: "v.semVer is valid but other semver.obj is nil", setupFunc: func(t testing.TB) (fuzzyVersion, *Version) { // create fuzzyVersion with valid semVer semVer, err := newSemanticVersion("1.2.3", false) require.NoError(t, err) fv := fuzzyVersion{ semVer: &semVer, raw: "1.2.3", } // this should create a version that when passed to newFuzzySemver // results in a semanticVersion with nil obj (this might be hard to achieve // but we'll test the logic path) otherVer := New("not-semver-compliant", UnknownFormat) return fv, otherVer }, expectFallback: true, }, { name: "both semvers are valid - should use semver comparison", setupFunc: func(t testing.TB) (fuzzyVersion, *Version) { // create fuzzyVersion with valid semVer semVer, err := newSemanticVersion("1.2.3", false) require.NoError(t, err) fv := fuzzyVersion{ semVer: &semVer, raw: "1.2.3", } otherVer := New("1.2.4", UnknownFormat) return fv, otherVer }, expectFallback: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fv, otherVer := tt.setupFunc(t) result, err := fv.Compare(otherVer) require.NoError(t, err) // verify that the result is a valid comparison result assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") // we can't easily test which path was taken without modifying the source, // but we can at least verify the function doesn't panic and returns valid results if tt.expectFallback { // when falling back to fuzzy comparison, we should get a result // the exact value depends on the fuzzyVersionComparison implementation assert.NotPanics(t, func() { _, _ = fv.Compare(otherVer) }) } }) } } ================================================ FILE: grype/version/gem_version.go ================================================ package version import ( "fmt" "regexp" "strconv" "strings" ) var _ Comparator = (*gemVersion)(nil) type gemVersion struct { original string segments []any canonical []any isPrerelease bool } const ( rubySegmentPattern = `(\d+|[a-zA-Z]+)` rubyCorrectnessPattern = `^[0-9a-zA-Z.\-]+$` ) var ( segmentRegexp = regexp.MustCompile(rubySegmentPattern) correctnessRegexp = regexp.MustCompile(rubyCorrectnessPattern) ) func newGemVersion(raw string) (gemVersion, error) { original := raw processed := cleanArchFromVersion(raw) if processed == "" || strings.TrimSpace(processed) == "" { processed = "0" } else { processed = strings.TrimSpace(processed) } if !correctnessRegexp.MatchString(processed) { return gemVersion{}, fmt.Errorf("malformed version number string %q", original) } processed = strings.ReplaceAll(processed, "-", ".pre.") isPrerelease := strings.ContainsAny(processed, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") segments, err := partitionSegments(processed) if err != nil { return gemVersion{}, fmt.Errorf("malformed version number string %q: %w", original, err) } if len(segments) == 0 { if processed == "0" { segments = []any{0} } else { return gemVersion{}, fmt.Errorf("malformed version number string %q (no valid segments after processing)", original) } } canonical := make([]any, len(segments)) copy(canonical, segments) canonical = trimTrailingZeros(canonical) canonical = trimIntermediateZeros(canonical, isPrerelease) if len(canonical) == 0 { canonical = []any{0} } return gemVersion{ original: original, segments: segments, canonical: canonical, isPrerelease: isPrerelease, }, nil } func (v gemVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newGemVersion(other.Raw) if err != nil { return 0, invalidFormatError(GemFormat, other.Raw, err) } return v.compare(o) } func (v gemVersion) compare(other gemVersion) (int, error) { result, commonSegmentsAreEqual, err := compareSegments(v.canonical, other.canonical) if err != nil { return -1, err } if commonSegmentsAreEqual { return compareLengths(v.canonical, other.canonical, result), nil } return result, nil } func (v *gemVersion) String() string { return v.original } func partitionSegments(versionString string) ([]any, error) { if versionString == "" { return []any{}, fmt.Errorf("cannot partition empty version string") } if strings.Contains(versionString, "..") { return nil, fmt.Errorf("invalid version string (double dot): %q", versionString) } if (strings.HasPrefix(versionString, ".") && versionString != ".") || (strings.HasSuffix(versionString, ".") && versionString != ".") { if len(versionString) > 1 { return nil, fmt.Errorf("invalid version string (leading/trailing dot): %q", versionString) } } parts := segmentRegexp.FindAllString(versionString, -1) if len(parts) == 0 { if versionString == "0" { return []any{0}, nil } return nil, fmt.Errorf("no valid segments found in %q", versionString) } segments := make([]any, 0, len(parts)) for _, s := range parts { if n, err := strconv.Atoi(s); err == nil { segments = append(segments, n) } else { segments = append(segments, s) } } return segments, nil } func trimTrailingZeros(segments []any) []any { if len(segments) <= 1 { if len(segments) == 1 { if num, ok := segments[0].(int); ok && num == 0 { return []any{0} } } return segments } lastSignificantIdx := -1 for i := len(segments) - 1; i >= 0; i-- { num, ok := segments[i].(int) if !ok || num != 0 { lastSignificantIdx = i break } // It's a numeric zero, continue looking } if lastSignificantIdx == -1 { return []any{0} } return segments[:lastSignificantIdx+1] } func trimIntermediateZeros(segments []any, isPrerelease bool) []any { if !isPrerelease || len(segments) == 0 { return segments } firstLetterIdx := -1 for i, seg := range segments { if _, ok := seg.(string); ok { firstLetterIdx = i break } } if firstLetterIdx == -1 { return segments } segmentsBeforeLetter := []any{} if firstLetterIdx > 0 { segmentsBeforeLetter = segments[:firstLetterIdx] } trimmedPrefix := []any{} if len(segmentsBeforeLetter) > 0 { lastNonZeroInPrefixIdx := -1 for i := len(segmentsBeforeLetter) - 1; i >= 0; i-- { num, ok := segmentsBeforeLetter[i].(int) if !ok || num != 0 { lastNonZeroInPrefixIdx = i break } } if lastNonZeroInPrefixIdx != -1 { trimmedPrefix = segmentsBeforeLetter[:lastNonZeroInPrefixIdx+1] } } reconstructed := make([]any, 0, len(trimmedPrefix)+len(segments)-firstLetterIdx) reconstructed = append(reconstructed, trimmedPrefix...) reconstructed = append(reconstructed, segments[firstLetterIdx:]...) return reconstructed } func compareSegments(left, right []any) (result int, allEqual bool, err error) { limit := len(left) if len(right) < limit { limit = len(right) } for i := 0; i < limit; i++ { l := left[i] r := right[i] lNum, lIsNum := l.(int) lStr, lIsStr := l.(string) rNum, rIsNum := r.(int) rStr, rIsStr := r.(string) if lIsNum && rIsNum { if lNum != rNum { if lNum < rNum { return -1, false, nil } return 1, false, nil } continue } if lIsStr && rIsStr { if cmp := strings.Compare(lStr, rStr); cmp != 0 { return cmp, false, nil } continue } if lIsNum && rIsStr { return 1, false, nil } if lIsStr && rIsNum { return -1, false, nil } return 0, false, fmt.Errorf("internal comparison error: unexpected types %T vs %T", l, r) } return 0, true, nil } func compareLengths(left, right []any, commonResult int) int { if commonResult != 0 { return commonResult } lLen := len(left) rLen := len(right) if lLen == rLen { return 0 } if lLen > rLen { for i := rLen; i < lLen; i++ { seg := left[i] if _, isStr := seg.(string); isStr { return -1 } if num, isNum := seg.(int); isNum && num != 0 { return 1 } } return 0 } for i := lLen; i < rLen; i++ { seg := right[i] if _, isStr := seg.(string); isStr { return 1 } if num, isNum := seg.(int); isNum && num != 0 { return -1 } } return 0 } func cleanArchFromVersion(raw string) string { platforms := []string{"x86", "universal", "arm", "java", "dalvik", "x64", "powerpc", "sparc", "mswin"} dash := "-" for _, p := range platforms { vals := strings.SplitN(raw, dash+p, 2) if len(vals) == 2 { return vals[0] } } return raw } ================================================ FILE: grype/version/gem_version_test.go ================================================ package version import ( "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGemVersion_Constraint(t *testing.T) { tests := []testCase{ // empty values {version: "2.3.1", constraint: "", satisfied: true}, // typical cases {version: "0.9.9-r0", constraint: "< 0.9.12-r1", satisfied: true}, // regression case {version: "1.5.0-arm-windows", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.2.0-arm-windows", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.0.1-armv5-window", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.0.1-armv7-linux", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0-universal-darwin-9", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0-universal-darwin-10", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0-x86_64-darwin-10", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "1.2.0", constraint: ">1.0, <2.0", satisfied: true}, {version: "1.2.0-x86", constraint: ">1.0, <2.0", satisfied: true}, {version: "1.2.0-x86-linux", constraint: ">1.0, <2.0", satisfied: true}, {version: "1.2.0-x86-linux", constraint: "= 1.2.0", satisfied: true}, {version: "1.2.0-x86_64-linux", constraint: "= 1.2.0", satisfied: true}, {version: "1.2.0-x86_64-linux", constraint: "< 1.2.1", satisfied: true}, // https://semver.org/#spec-item-11 {version: "1.2.0-alpha-x86-linux", constraint: "<1.2.0", satisfied: true}, {version: "1.2.0-alpha-1-x86-linux", constraint: "<1.2.0", satisfied: true}, // gem versions seem to respect the order: {sem-version}+{meta}-{arch}-{os} // but let's check the extraction works even when the order of {meta}-{arch} varies. {version: "1.2.0-alpha-1-x86-linux-meta", constraint: "<1.2.0", satisfied: true}, {version: "1.2.0-alpha-1-meta-x86-linux", constraint: "<1.2.0", satisfied: true}, {version: "1.2.0-alpha-1-x86-linux-meta", constraint: ">1.1.0", satisfied: true}, {version: "1.2.0-alpha-1-arm-linux-meta", constraint: ">1.1.0", satisfied: true}, {version: "1.0.0-alpha-a.b-c-somethinglong-build.1-aef.1-its-okay", constraint: "<1.0.0", satisfied: true}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, GemFormat) assert.NoError(t, err, "unexpected error from newSemanticConstraint: %v", err) test.assertVersionConstraint(t, GemFormat, constraint) }) } } func Test_cleanPlatformMakesEqualVersions(t *testing.T) { tests := []struct { input string trimmed string want *gemVersion }{ {input: "1.13.1", trimmed: "1.13.1"}, {input: "1.13.1-arm-linux", trimmed: "1.13.1"}, {input: "1.13.1-armv6-linux", trimmed: "1.13.1"}, {input: "1.13.1-armv7-linux", trimmed: "1.13.1"}, {input: "1.13.1-java", trimmed: "1.13.1"}, {input: "1.13.1-dalvik", trimmed: "1.13.1"}, {input: "1.13.1-mswin32", trimmed: "1.13.1"}, {input: "1.13.1-x64-mswin64", trimmed: "1.13.1"}, {input: "1.13.1-sparc-unix", trimmed: "1.13.1"}, {input: "1.13.1-powerpc-darwin", trimmed: "1.13.1"}, {input: "1.13.1-x86-linux", trimmed: "1.13.1"}, {input: "1.13.1-x86_64-linux", trimmed: "1.13.1"}, {input: "1.13.1-x86-freebsd", trimmed: "1.13.1"}, {input: "1.13.1-x86-mswin32-80", trimmed: "1.13.1"}, {input: "1.13.1-universal-darwin-8", trimmed: "1.13.1"}, // ruby versions get the canonical segment "pre" if there are any segments that are all // alphabetic characters. {input: "1.13.1-beta-universal-darwin-8", trimmed: "1.13.1.pre.beta"}, {input: "1.13.1-alpha-1-meta-arm-linux", trimmed: "1.13.1-alpha-1-meta"}, {input: "1.13.1-alpha-1-build.12-arm-linux", trimmed: "1.13.1-alpha-1-build.12"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { original := New(tt.input, GemFormat) trimmed := New(tt.trimmed, GemFormat) comp, err := original.Compare(trimmed) require.NoError(t, err) assert.Equal(t, 0, comp) comp, err = trimmed.Compare(original) require.NoError(t, err) assert.Equal(t, 0, comp) }) } } func TestNewGemVersion_ValidInputs(t *testing.T) { tests := []struct { input string expectedOriginal string // What v.original should be expectedSegments []any // What v.segments should be (after .pre. processing) expectedPrerelease bool }{ {"1.0", "1.0", []any{1, 0}, false}, {"1.0 ", "1.0 ", []any{1, 0}, false}, // original preserves space {" 1.0 ", " 1.0 ", []any{1, 0}, false}, {"1.2.3", "1.2.3", []any{1, 2, 3}, false}, {"1.2.3.a", "1.2.3.a", []any{1, 2, 3, "a"}, true}, {"1.2.3-b4", "1.2.3-b4", []any{1, 2, 3, "pre", "b", 4}, true}, {"1", "1", []any{1}, false}, {"0", "0", []any{0}, false}, {"", "", []any{0}, false}, // Empty string becomes "0" effectively, original is "" {" ", " ", []any{0}, false}, // Whitespace string becomes "0" effectively {"1.0-alpha", "1.0-alpha", []any{1, 0, "pre", "alpha"}, true}, {"1-1", "1-1", []any{1, "pre", 1}, true}, } for _, tt := range tests { t.Run(fmt.Sprintf("Input_%s", tt.input), func(t *testing.T) { v, err := newGemVersion(tt.input) require.NoError(t, err) assert.Equal(t, tt.expectedOriginal, v.original, "Original string mismatch") assert.Equal(t, tt.expectedSegments, v.segments, "Initial segments mismatch") assert.Equal(t, tt.expectedPrerelease, v.isPrerelease, "Prerelease flag mismatch") }) } } func TestNewGemVersion_InvalidInputs(t *testing.T) { invalidVersions := []struct { name string input string errorSubstring string }{ {"newline", "1.0\n2.0", "malformed version number string"}, {"double_dot", "1..2", "malformed version number string"}, {"space_separated", "1.2 3.4", "malformed version number string"}, {"trailing_dot_long", "1.2.", "leading/trailing dot"}, {"leading_dot_long", ".1.2", "leading/trailing dot"}, {"just_dot", ".", "no valid segments"}, {"double_hyphen", "--", "malformed version number string"}, {"hyphen_dot", "1.-2", "malformed version number string"}, {"dot_hyphen", "1.-pre", "malformed version number string"}, {"underscore", "1_2", "malformed version number string"}, {"empty_segments", "...", "malformed version number string"}, {"invalid_segment_char", "1.2.a@b", "malformed version number string"}, } for _, tt := range invalidVersions { t.Run(tt.name, func(t *testing.T) { _, err := newGemVersion(tt.input) require.Error(t, err) if tt.errorSubstring != "" { assert.Contains(t, err.Error(), tt.errorSubstring, "Error message mismatch for input: %s", tt.input) } }) } } func TestGemVersion_Compare(t *testing.T) { tests := []struct { v1 string v2 string want int // expected result of v1.Compare(v2) }{ // Basic comparisons (from Ruby's test_spaceship) {"1.0", "1.0.0", 0}, {"1.0", "1.0.a", 1}, {"1.8.2", "0.0.0", 1}, {"1.8.2", "1.8.2.a", 1}, {"1.8.2.b", "1.8.2.a", 1}, {"1.8.2.a", "1.8.2", -1}, {"1.8.2.a10", "1.8.2.a9", 1}, {"", "0", 0}, // "" is treated as "0" // Canonicalization leading to equality {"0.beta.1", "0.0.beta.1", 0}, // Ruby: 0.beta.1 <=> 0.0.beta.1 is 0. Canonical for both is ["beta", -1] {"0.0.beta", "0.0.beta.1", -1}, // Ruby: 0.0.beta <=> 0.0.beta.1 is -1. Canonical ["beta"] vs ["beta", -1] // String segments comparison {"5.a", "5.0.0.rc2", -1}, // "a" < "rc" {"5.x", "5.0.0.rc2", 1}, // "x" > "rc" // Direct string comparison from Ruby test {"1.9.3", "1.9.3", 0}, {"1.9.3", "1.9.2.99", 1}, {"1.9.3", "1.9.3.1", -1}, // Additional common cases {"1.0", "1.1", -1}, {"1.1", "1.0", 1}, {"1", "1.0", 0}, {"1.0.1", "1.0.0", 1}, {"1.0.0", "1.0.1", -1}, // Prerelease vs Prerelease (length diff) {"1.0.alpha.1", "1.0.alpha", 1}, {"1.0.alpha", "1.0.alpha.1", -1}, // Hyphen handling (SemVer-like via .pre.) {"1.0.0-alpha", "1.0.0-alpha.1", -1}, {"1.0.0-alpha.1", "1.0.0-beta.2", -1}, {"1.0.0-beta.2", "1.0.0-beta.11", -1}, {"1.0.0-beta.11", "1.0.0-rc.1", -1}, // beta < rc {"1.0.0-rc1", "1.0.0", -1}, {"1.0.0-1", "1", -1}, // 1.0.0.pre.1 vs 1 {"1-1", "1", -1}, // 1.pre.1 vs 1 // From Ruby's test_semver (some overlap, ensure coverage) {"1.0.0-alpha", "1.0.0-alpha.1", -1}, {"1.0.0-alpha.1", "1.0.0-beta.2", -1}, // alpha < beta {"1.0.0-beta.2", "1.0.0-beta.11", -1}, // 2 < 11 {"1.0.0-beta.11", "1.0.0-rc.1", -1}, // beta < rc {"1.0.0-rc1", "1.0.0", -1}, // 1.0.0.pre.rc.1 < 1.0.0 (release) {"1.0.0-1", "1", -1}, // 1.0.0.pre.1 < 1 (release) // Edge cases with canonicalization {"1.0", "1", 0}, {"1.0.0", "1", 0}, {"1.a", "1.0.0.a", 0}, // Canonical [1,"a"] for both {"1.a.0", "1.a", 0}, // Canonical [1,"a"] for both } for _, tt := range tests { t.Run(fmt.Sprintf("%s_vs_%s", tt.v1, tt.v2), func(t *testing.T) { ver1 := New(tt.v1, GemFormat) ver2 := New(tt.v2, GemFormat) // Test v1 vs v2 got1, err1 := ver1.Compare(ver2) require.NoError(t, err1, "v1.Compare(v2) failed for %s vs %s", tt.v1, tt.v2) assert.Equal(t, tt.want, got1, "Compare(%q, %q) == %d, want %d", tt.v1, tt.v2, got1, tt.want) // Test symmetry: v2 vs v1 expectedSymmetric := 0 if tt.want != 0 { expectedSymmetric = -tt.want } got2, err2 := ver2.Compare(ver1) require.NoError(t, err2, "v2.Compare(v1) failed for %s vs %s", tt.v2, tt.v1) assert.Equal(t, expectedSymmetric, got2, "Compare(%q, %q) == %d, want %d (symmetric)", tt.v2, tt.v1, got2, expectedSymmetric) // Test reflexivity: v1 vs v1 gotReflexive1, errReflexive1 := ver1.Compare(ver1) require.NoError(t, errReflexive1, "v1.Compare(v1) failed for %s", tt.v1) assert.Equal(t, 0, gotReflexive1, "Compare(%q, %q) == %d, want 0 (reflexive)", tt.v1, tt.v1, gotReflexive1) }) } } func TestGemVersion_Compare_Errors(t *testing.T) { vGem1_0, err := newGemVersion("1.0") require.NoError(t, err) t.Run("CompareWithNil", func(t *testing.T) { _, err := vGem1_0.Compare(nil) assert.ErrorIs(t, err, ErrNoVersionProvided) }) t.Run("CompareWithDifferentFormat", func(t *testing.T) { // Assuming SemanticFormat is a distinct, incompatible format // and that the Format type has a String() method for user-friendly error messages. vOther := &Version{Raw: "1.0.0", Format: SemanticFormat} _, err := vGem1_0.Compare(vOther) require.NoError(t, err) }) t.Run("CompareWithUnknownFormat_ParsableAsGem", func(t *testing.T) { vOther := &Version{Raw: "1.1", Format: UnknownFormat} // Parsable as Gem res, err := vGem1_0.Compare(vOther) assert.NoError(t, err) assert.Equal(t, -1, res) // 1.0 < 1.1 }) t.Run("CompareWithUnknownFormat_UnparsableAsGem", func(t *testing.T) { vOther := &Version{Raw: "invalid..version", Format: UnknownFormat} _, err := vGem1_0.Compare(vOther) require.Error(t, err) require.ErrorContains(t, err, "malformed version number string") }) } func TestGemVersion_canonical(t *testing.T) { tests := []struct { name string version string want []any }{ // obtained from a simple ruby program like this: /* require 'rubygems/version' v = Gem::Version.new(input) v.canonical_segments */ {"simple ints", "1.2.3", []any{1, 2, 3}}, {"leading zeros preserved", "0.1.2", []any{0, 1, 2}}, {"drop intermediate zeros in pre-release", "5.0.0.a1", []any{5, "a", 1}}, {"preserve intermedia zeros in regular release", "1.0.0.1", []any{1, 0, 0, 1}}, {"drop trailing zeros", "1.0.0", []any{1}}, {"alpha version", "1.6.1.a", []any{1, 6, 1, "a"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v, err := newGemVersion(tt.version) require.NoError(t, err) if d := cmp.Diff(v.canonical, tt.want); d != "" { t.Errorf("canonical mismatch (-want +got):\n%s", d) } }) } } ================================================ FILE: grype/version/generic_constraint.go ================================================ package version import ( "fmt" "strings" ) var _ Constraint = (*genericConstraint)(nil) type genericConstraint struct { Raw string Expression simpleRangeExpression Fmt Format } func newGenericConstraint(format Format, raw string) (genericConstraint, error) { constraints, err := parseRangeExpression(raw) if err != nil { return genericConstraint{}, invalidFormatError(format, raw, err) } return genericConstraint{ Expression: constraints, Raw: raw, Fmt: format, }, nil } func (g genericConstraint) String() string { value := g.Value() if g.Raw == "" { value = "none" } return fmt.Sprintf("%s (%s)", value, strings.ToLower(g.Fmt.String())) } func (g genericConstraint) Value() string { return g.Raw } func (g genericConstraint) Format() Format { return g.Fmt } func (g genericConstraint) Satisfied(version *Version) (bool, error) { if g.Raw == "" && version != nil { // empty constraints are always satisfied return true, nil } if version == nil { if g.Raw != "" { // a non-empty constraint with no version given should always fail return false, nil } return true, nil } // we want to prevent against two known formats that are different from being compared. // if the passed in version is unknown, we allow the comparison to proceed if version.Format != g.Fmt && version.Format != UnknownFormat { return false, newUnsupportedFormatError(g.Fmt, version) } return g.Expression.satisfied(g.Fmt, version) } ================================================ FILE: grype/version/generic_constraint_test.go ================================================ package version import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenericConstraint_String(t *testing.T) { tests := []struct { name string constraint string format Format expected string }{ { name: "empty constraint", constraint: "", format: SemanticFormat, expected: "none (semantic)", }, { name: "simple constraint", constraint: "> 1.0.0", format: SemanticFormat, expected: "> 1.0.0 (semantic)", }, { name: "complex constraint", constraint: ">= 1.0.0, < 2.0.0", format: MavenFormat, expected: ">= 1.0.0, < 2.0.0 (maven)", }, { name: "jvm format name", constraint: "< 11", format: JVMFormat, expected: "< 11 (jvm)", }, { name: "go format name", constraint: "> v1.2.3", format: GolangFormat, expected: "> v1.2.3 (go)", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { constraint, err := newGenericConstraint(test.format, test.constraint) require.NoError(t, err) result := constraint.String() assert.Equal(t, test.expected, result) }) } } func TestGenericConstraint_Satisfied_EmptyConstraint(t *testing.T) { constraint, err := newGenericConstraint(SemanticFormat, "") require.NoError(t, err) tests := []struct { name string version *Version }{ { name: "with valid version", version: New("1.2.3", SemanticFormat), }, { name: "with nil version", version: nil, }, { name: "with different format version", version: New("1.2.3-r1", ApkFormat), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { satisfied, err := constraint.Satisfied(test.version) assert.NoError(t, err) assert.True(t, satisfied, "empty constraint should always be satisfied") }) } } func TestGenericConstraint_Satisfied_WithConstraint(t *testing.T) { tests := []struct { name string constraint string version string satisfied bool shouldError bool }{ { name: "simple greater than - satisfied", constraint: "> 1.0.0", version: "1.2.3", satisfied: true, }, { name: "simple greater than - not satisfied", constraint: "> 2.0.0", version: "1.2.3", satisfied: false, }, { name: "complex constraint - satisfied", constraint: ">= 1.0.0, < 2.0.0", version: "1.5.0", satisfied: true, }, { name: "complex constraint - not satisfied", constraint: ">= 1.0.0, < 2.0.0", version: "2.5.0", satisfied: false, }, { name: "equality constraint - satisfied", constraint: "= 1.2.3", version: "1.2.3", satisfied: true, }, { name: "equality constraint - not satisfied", constraint: "= 1.2.3", version: "1.2.4", satisfied: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { constraint, err := newGenericConstraint(SemanticFormat, test.constraint) require.NoError(t, err) version := New(test.version, SemanticFormat) satisfied, err := constraint.Satisfied(version) if test.shouldError { require.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, test.satisfied, satisfied) } }) } } func TestGenericConstraint_Invalid(t *testing.T) { tests := []struct { name string constraint string gen func(unit rangeUnit) (Comparator, error) }{ { name: "invalid operator", constraint: "~~ 1.0.0", }, { name: "malformed constraint", constraint: "> 1.0.0 < 2.0.0", // missing comma }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := newGenericConstraint(SemanticFormat, test.constraint) require.Error(t, err) }) } } func TestGenericConstraint_Satisfied_UnknownFormatComparison(t *testing.T) { tests := []struct { name string constraintFmt Format constraint string versionStr string versionFmt Format expectedResult bool wantErr require.ErrorAssertionFunc }{ { name: "semantic constraint with unknown format version - satisfied", constraintFmt: SemanticFormat, constraint: "> 1.0.0", versionStr: "1.2.3", versionFmt: UnknownFormat, expectedResult: true, }, { name: "semantic constraint with unknown format version - not satisfied", constraintFmt: SemanticFormat, constraint: "> 2.0.0", versionStr: "1.2.3", versionFmt: UnknownFormat, expectedResult: false, }, { name: "maven constraint with unknown format version - satisfied", constraintFmt: MavenFormat, constraint: ">= 1.0.0", versionStr: "1.5.0", versionFmt: UnknownFormat, expectedResult: true, }, { name: "different known formats should error", constraintFmt: SemanticFormat, constraint: "> 1.0.0", versionStr: "1.2.3-r1", versionFmt: ApkFormat, expectedResult: false, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } constraint, err := newGenericConstraint(tt.constraintFmt, tt.constraint) require.NoError(t, err) version := New(tt.versionStr, tt.versionFmt) satisfied, err := constraint.Satisfied(version) tt.wantErr(t, err) if err != nil { return } require.Equal(t, tt.expectedResult, satisfied) }) } } ================================================ FILE: grype/version/golang_version.go ================================================ package version import ( "fmt" "strings" hashiVer "github.com/anchore/go-version" ) var _ Comparator = (*golangVersion)(nil) type golangVersion struct { raw string obj *hashiVer.Version } func newGolangVersion(v string) (golangVersion, error) { if v == "(devel)" { return golangVersion{}, ErrUnsupportedVersion } // Invalid Semver fix ups // go stdlib is reported by syft as a go package with version like "go1.24.1" // other versions have "v" as a prefix, which the semver lib handles automatically fixedUp := strings.TrimPrefix(v, "go") // go1.24 creates non-dot separated build metadata fields, e.g. +incompatible+dirty // Fix up as per semver spec before, after, found := strings.Cut(fixedUp, "+") if found { fixedUp = before + "+" + strings.ReplaceAll(after, "+", ".") } semver, err := hashiVer.NewSemver(fixedUp) if err != nil { return golangVersion{}, err } return golangVersion{ raw: v, obj: semver, }, nil } func (v golangVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newGolangVersion(other.Raw) if err != nil { return 0, err } if o.raw == v.raw { return 0, nil } if o.raw == "(devel)" { return -1, fmt.Errorf("cannot compare a non-development version %q with a default development version of %q", v.raw, o.raw) } return v.compare(o), nil } func (v golangVersion) compare(o golangVersion) int { switch { case v.obj != nil && o.obj != nil: return v.obj.Compare(o.obj) case v.obj != nil && o.obj == nil: return 1 case v.obj == nil && o.obj != nil: return -1 default: return strings.Compare(v.raw, o.raw) } } ================================================ FILE: grype/version/golang_version_test.go ================================================ package version import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" hashiVer "github.com/anchore/go-version" ) func TestGolangVersion_Constraint(t *testing.T) { tests := []struct { name string version string constraint string satisfied bool }{ { name: "regular semantic version satisfied", version: "v1.2.3", constraint: "< 1.2.4", satisfied: true, }, { name: "regular semantic version unsatisfied", version: "v1.2.3", constraint: "> 1.2.4", satisfied: false, }, { name: "+incompatible added to version", // see grype#1581 version: "v3.2.0+incompatible", constraint: "<=3.2.0", satisfied: true, }, { name: "the empty constraint is always satisfied", version: "v1.0.0", constraint: "", satisfied: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c, err := GetConstraint(tc.constraint, GolangFormat) require.NoError(t, err) v := New(tc.version, GolangFormat) sat, err := c.Satisfied(v) require.NoError(t, err) assert.Equal(t, tc.satisfied, sat) }) } } func TestGolangVersion_String(t *testing.T) { tests := []struct { name string constraint string expected string }{ { name: "empty string", constraint: "", expected: "none (go)", }, { name: "basic constraint", constraint: "< 1.3.4", expected: "< 1.3.4 (go)", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c, err := GetConstraint(tc.constraint, GolangFormat) require.NoError(t, err) assert.Equal(t, tc.expected, c.String()) }) } } func TestGolangVersion_Compare(t *testing.T) { tests := []struct { name string version1 string version2 string expected int }{ { name: "same basic version", version1: "v1.2.3", version2: "v1.2.3", expected: 0, }, { name: "same version with incompatible", version1: "v3.2.0+incompatible", version2: "v3.2.0+incompatible", expected: 0, }, { name: "same go stdlib version", version1: "go1.24.1", version2: "go1.24.1", expected: 0, }, { name: "version1 less than version2", version1: "v1.2.3", version2: "v1.2.4", expected: -1, }, { name: "version1 greater than version2", version1: "v1.2.4", version2: "v1.2.3", expected: 1, }, { name: "version1 equal to version2", version1: "v1.2.3", version2: "v1.2.3", expected: 0, }, { name: "go stdlib versions", version1: "go1.23.1", version2: "go1.24.1", expected: -1, }, { name: "incompatible versions", version1: "v3.1.0+incompatible", version2: "v3.2.0+incompatible", expected: -1, }, { name: "semver this version less", version1: "v1.2.3", version2: "v1.2.4", expected: -1, }, { name: "semver this version more", version1: "v1.3.4", version2: "v1.2.4", expected: 1, }, { name: "semver equal", version1: "v1.2.4", version2: "v1.2.4", expected: 0, }, { name: "commit-sha this version less", version1: "v0.0.0-20180116102854-5a71ef0e047d", version2: "v0.0.0-20190116102854-somehash", expected: -1, }, { name: "commit-sha this version more", version1: "v0.0.0-20180216102854-5a71ef0e047d", version2: "v0.0.0-20180116102854-somehash", expected: 1, }, { name: "commit-sha this version equal", version1: "v0.0.0-20180116102854-5a71ef0e047d", version2: "v0.0.0-20180116102854-5a71ef0e047d", expected: 0, }, { name: "this pre-semver is less than any semver", version1: "v0.0.0-20180116102854-5a71ef0e047d", version2: "v0.0.1", expected: -1, }, { name: "semver is greater than timestamp", version1: "v2.1.0", version2: "v0.0.0-20180116102854-5a71ef0e047d", expected: 1, }, { name: "pseudoversion less than other pseudoversion", version1: "v0.0.0-20170116102854-1ef0e047d5a7", version2: "v0.0.0-20180116102854-5a71ef0e047d", expected: -1, }, { name: "pseudoversion greater than other pseudoversion", version1: "v0.0.0-20190116102854-8a3f0e047d5a", version2: "v0.0.0-20180116102854-5a71ef0e047d", expected: 1, }, { name: "+incompatible doesn't break equality", version1: "v3.2.0", version2: "v3.2.0+incompatible", expected: 0, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { version1 := New(test.version1, GolangFormat) version2 := New(test.version2, GolangFormat) result, err := version1.Compare(version2) assert.NoError(t, err) assert.Equal(t, test.expected, result) }) } } func TestGolangVersion_Compare_NilVersion(t *testing.T) { version := New("v1.2.3", GolangFormat) result, err := version.Compare(nil) require.Error(t, err) assert.Equal(t, ErrNoVersionProvided, err) assert.Equal(t, -1, result) } func TestGolangVersion_Compare_DifferentFormat(t *testing.T) { golangVer, err := newGolangVersion("v1.2.3") require.NoError(t, err) semanticVer := New("1.2.3", SemanticFormat) result, err := golangVer.Compare(semanticVer) require.NoError(t, err) assert.Equal(t, 0, result) } func TestGolangVersion(t *testing.T) { tests := []struct { name string input string expected golangVersion wantErr require.ErrorAssertionFunc }{ { name: "normal semantic version", input: "v1.8.0", expected: golangVersion{ raw: "v1.8.0", obj: hashiVer.Must(hashiVer.NewSemver("v1.8.0")), }, }, { name: "v0.0.0 date and hash version", input: "v0.0.0-20180116102854-5a71ef0e047d", expected: golangVersion{ raw: "v0.0.0-20180116102854-5a71ef0e047d", obj: hashiVer.Must(hashiVer.NewSemver("v0.0.0-20180116102854-5a71ef0e047d")), }, }, { name: "semver with +incompatible", input: "v24.0.7+incompatible", expected: golangVersion{ raw: "v24.0.7+incompatible", obj: hashiVer.Must(hashiVer.NewSemver("v24.0.7+incompatible")), }, }, { name: "semver with +incompatible+dirty", input: "v24.0.7+incompatible+dirty", expected: golangVersion{ raw: "v24.0.7+incompatible+dirty", obj: hashiVer.Must(hashiVer.NewSemver("v24.0.7+incompatible.dirty")), }, }, { name: "standard library", input: "go1.21.4", expected: golangVersion{ raw: "go1.21.4", obj: hashiVer.Must(hashiVer.NewSemver("1.21.4")), }, }, { // "(devel)" is the main module of a go program. // If we get a package with this version, it means the SBOM // doesn't have a real version number for the built package, so // we can't compare it and should just return an error. name: "devel", input: "(devel)", wantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) { require.ErrorIs(t, err, ErrUnsupportedVersion) }, }, { name: "invalid", input: "invalid", wantErr: require.Error, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.wantErr == nil { tc.wantErr = require.NoError } v, err := newGolangVersion(tc.input) tc.wantErr(t, err) if err != nil { return } assert.Equal(t, tc.expected, v) }) } } ================================================ FILE: grype/version/helper_test.go ================================================ package version import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type testCase struct { name string version string constraint string satisfied bool wantError require.ErrorAssertionFunc } func (c *testCase) tName() string { if c.name != "" { return c.name } return fmt.Sprintf("ver='%s'const='%s'", c.version, strings.ReplaceAll(c.constraint, " ", "")) } func (c *testCase) assertVersionConstraint(t *testing.T, format Format, constraint Constraint) { t.Helper() if c.wantError == nil { c.wantError = require.NoError } version := New(c.version, format) isSatisfied, err := constraint.Satisfied(version) c.wantError(t, err) if err != nil { return } assert.Equal(t, c.satisfied, isSatisfied, "unexpected constraint check result") } ================================================ FILE: grype/version/jvm_version.go ================================================ package version import ( "fmt" "regexp" "strings" hashiVer "github.com/anchore/go-version" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" ) var _ interface { Comparator } = (*jvmVersion)(nil) var ( preJep223VersionPattern = regexp.MustCompile(`^1\.(?P\d+)(\.(?P\d+)([_-](update)?(_)?(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?)?`) nonCompliantSemverIsh = regexp.MustCompile(`^(?P\d+)(\.(?P\d+)(\.(?P\d+))?([_-](update)?(_)?(?P\d+))?(-(?P[^b][^-]+))?(-b(?P\d+))?)?`) ) type jvmVersion struct { isPreJep223 bool semVer *hashiVer.Version } func newJvmVersion(raw string) (jvmVersion, error) { isPreJep233 := strings.HasPrefix(raw, "1.") if isPreJep233 { // convert the pre-JEP 223 version to semver raw = convertPreJep223Version(raw) } else { raw = convertNonCompliantSemver(raw) } verObj, err := hashiVer.NewVersion(raw) if err != nil { return jvmVersion{}, invalidFormatError(JVMFormat, raw, err) } return jvmVersion{ isPreJep223: isPreJep233, semVer: verObj, }, nil } func (v jvmVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newJvmVersion(other.Raw) if err != nil { return 0, err } return v.compare(o), nil } func (v jvmVersion) compare(other jvmVersion) int { return v.semVer.Compare(other.semVer) } func convertNonCompliantSemver(version string) string { // if there is -update as a prerelease, and the patch version is missing or 0, then we should parse the prerelease // info that has the update value and extract the version. This should be used as the patch version. // 8.0-update302 --> 8.0.302 // 8.0-update302-b08 --> 8.0.302+8 // 8.0-update_302-b08 --> 8.0.302+8 matches := internal.MatchNamedCaptureGroups(nonCompliantSemverIsh, version) if len(matches) == 0 { log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version") return version } // extract relevant parts from the matches majorVersion := trim0sFromLeft(matches["major"]) minorVersion := trim0sFromLeft(matches["minor"]) patchVersion := trim0sFromLeft(matches["patch"]) update := trim0sFromLeft(matches["update"]) preRelease := trim0sFromLeft(matches["prerelease"]) build := trim0sFromLeft(matches["build"]) if (patchVersion == "" || patchVersion == "0") && update != "" { patchVersion = update } return buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build) } func convertPreJep223Version(version string) string { // convert the following pre JEP 223 version strings to semvers // 1.8.0_302-b08 --> 8.0.302+8 // 1.9.0-ea-b19 --> 9.0.0-ea+19 // NOTE: this makes an assumption that the old update field is the patch version in semver... // this is NOT strictly in the spec, but for 1.8 this tends to be true (especially for temurin-based builds) version = strings.TrimSpace(version) matches := internal.MatchNamedCaptureGroups(preJep223VersionPattern, version) if len(matches) == 0 { log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version") return version } // extract relevant parts from the matches majorVersion := trim0sFromLeft(matches["major"]) minorVersion := trim0sFromLeft(matches["minor"]) patchVersion := trim0sFromLeft(matches["patch"]) preRelease := trim0sFromLeft(matches["prerelease"]) build := trim0sFromLeft(matches["build"]) if patchVersion == "" { patchVersion = "0" } return buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build) } func buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build string) string { if minorVersion == "" { minorVersion = "0" } segs := []string{majorVersion, minorVersion} if patchVersion != "" { segs = append(segs, patchVersion) } var semver strings.Builder semver.WriteString(strings.Join(segs, ".")) if preRelease != "" { fmt.Fprintf(&semver, "-%s", preRelease) } if build != "" { fmt.Fprintf(&semver, "+%s", build) } return semver.String() } func trim0sFromLeft(v string) string { if v == "0" { return v } return strings.TrimLeft(v, "0") } ================================================ FILE: grype/version/jvm_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/require" ) func TestJVMVersion_Constraint(t *testing.T) { tests := []testCase{ // pre jep 223 versions {version: "1.7.0_80", constraint: "< 1.8.0", satisfied: true}, {version: "1.8.0_131", constraint: "> 1.8.0", satisfied: true}, {version: "1.8.0_131", constraint: "< 1.8.0_132", satisfied: true}, {version: "1.8.0_131-b11", constraint: "< 1.8.0_132", satisfied: true}, {version: "1.7.0_80", constraint: "> 1.8.0", satisfied: false}, {version: "1.8.0_131", constraint: "< 1.8.0", satisfied: false}, {version: "1.8.0_131", constraint: "> 1.8.0_132", satisfied: false}, {version: "1.8.0_131-b11", constraint: "> 1.8.0_132", satisfied: false}, {version: "1.7.0_80", constraint: "= 1.8.0", satisfied: false}, {version: "1.8.0_131", constraint: "= 1.8.0", satisfied: false}, {version: "1.8.0_131", constraint: "= 1.8.0_132", satisfied: false}, {version: "1.8.0_131-b11", constraint: "= 1.8.0_132", satisfied: false}, {version: "1.8.0_80", constraint: "= 1.8.0_80", satisfied: true}, {version: "1.8.0_131", constraint: ">= 1.8.0_131", satisfied: true}, {version: "1.8.0_131", constraint: "= 1.8.0_131-b001", satisfied: true}, // builds should not matter {version: "1.8.0_131-ea-b11", constraint: "= 1.8.0_131-ea", satisfied: true}, // jep 223 versions {version: "8.0.4", constraint: "> 8.0.3", satisfied: true}, {version: "8.0.4", constraint: "< 8.0.5", satisfied: true}, {version: "9.0.0", constraint: "> 8.0.5", satisfied: true}, {version: "9.0.0", constraint: "< 9.1.0", satisfied: true}, {version: "11.0.4", constraint: "<= 11.0.4", satisfied: true}, {version: "11.0.5", constraint: "> 11.0.4", satisfied: true}, {version: "8.0.4", constraint: "< 8.0.3", satisfied: false}, {version: "8.0.4", constraint: "> 8.0.5", satisfied: false}, {version: "9.0.0", constraint: "< 8.0.5", satisfied: false}, {version: "9.0.0", constraint: "> 9.1.0", satisfied: false}, {version: "11.0.4", constraint: "> 11.0.4", satisfied: false}, {version: "11.0.5", constraint: "< 11.0.4", satisfied: false}, // mixed versions {version: "1.8.0_131", constraint: "< 9.0.0", satisfied: true}, // 1.8.0_131 -> 8.0.131 {version: "9.0.0", constraint: "> 1.8.0_131", satisfied: true}, // 1.8.0_131 -> 8.0.131 {version: "1.8.0_131", constraint: "<= 8.0.131", satisfied: true}, {version: "1.8.0_131", constraint: "> 7.0.79", satisfied: true}, {version: "1.8.0_131", constraint: "= 8.0.131", satisfied: true}, {version: "1.8.0_131", constraint: ">= 9.0.0", satisfied: false}, {version: "9.0.1", constraint: "< 8.0.131", satisfied: false}, // pre-release versions {version: "1.8.0_131-ea", constraint: "< 1.8.0_131", satisfied: true}, {version: "1.8.0_131", constraint: "> 1.8.0_131-ea", satisfied: true}, {version: "9.0.0-ea", constraint: "< 9.0.0", satisfied: true}, {version: "9.0.0-ea", constraint: "> 1.8.0_131", satisfied: true}, } for _, test := range tests { t.Run(test.version+"_constraint_"+test.constraint, func(t *testing.T) { constraint, err := GetConstraint(test.constraint, JVMFormat) require.NoError(t, err) test.assertVersionConstraint(t, JVMFormat, constraint) }) } } func TestJVMVersion_Compare(t *testing.T) { tests := []struct { v1 string v2 string expected int }{ // pre jep223 versions {"1.8", "1.8.0", 0}, {"1.8.0", "1.8.0_0", 0}, {"1.8.0", "1.8.0", 0}, {"1.7.0", "1.8.0", -1}, {"1.8.0_131", "1.8.0_131", 0}, {"1.8.0_131", "1.8.0_132", -1}, // builds should not matter {"1.8.0_131", "1.8.0_130", 1}, {"1.8.0_131", "1.8.0_132-b11", -1}, {"1.8.0_131-b11", "1.8.0_132-b11", -1}, {"1.8.0_131-b11", "1.8.0_131-b12", 0}, {"1.8.0_131-b11", "1.8.0_131-b10", 0}, {"1.8.0_131-b11", "1.8.0_131", 0}, {"1.8.0_131-b11", "1.8.0_131-b11", 0}, // jep223 versions (semver) {"8.0.4", "8.0.4", 0}, {"8.0.4", "8.0.5", -1}, {"8.0.4", "8.0.3", 1}, {"8.0.4", "8.0.4+b1", 0}, // mix comparison {"1.8.0_131", "8.0.4", 1}, // 1.8.0_131 --> 8.0.131 {"8.0.4", "1.8.0_131", -1}, // doesn't matter which side the comparison is on {"1.8.0_131-b002", "8.0.131+b2", 0}, // builds should not matter {"1.8.0_131-b002", "8.0.131+b1", 0}, // builds should not matter {"1.6.0", "8.0.1", -1}, // 1.6.0 --> 6.0.0 // prerelease {"1.8.0_13-ea-b002", "1.8.0_13-ea-b001", 0}, {"1.8.0_13-ea", "1.8.0_13-ea-b001", 0}, {"1.8.0_13-ea-b002", "8.0.13-ea+b2", 0}, {"1.8.0_13-ea-b002", "8.0.13+b2", -1}, {"1.8.0_13-b002", "8.0.13-ea+b2", 1}, // pre 1.8 (when the jep 223 was introduced) {"1.7.0", "7.0.0", 0}, // there is no v7 of the JVM, but we want to honor this comparison since it may be someone mistakenly using the wrong version format // invalid but we should work with these {"1.8.0_131", "1.8.0-update131-b02", 0}, {"1.8.0_131", "1.8.0-update_131-b02", 0}, } for _, test := range tests { name := test.v1 + "_vs_" + test.v2 t.Run(name, func(t *testing.T) { v1 := New(test.v1, JVMFormat) require.NotNil(t, v1) v2 := New(test.v2, JVMFormat) require.NotNil(t, v2) actual, err := v1.Compare(v2) require.NoError(t, err) require.Equal(t, test.expected, actual) }) } } func TestJVMVersion_ConvertNonCompliantSemver(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "simple update", input: "8.0-update302", expected: "8.0.302", }, { name: "update with build", input: "8.0-update302-b08", expected: "8.0.302+8", }, { name: "update with underscore and build", input: "8.0-update_302-b08", expected: "8.0.302+8", }, { name: "version without patch and prerelease", input: "8.0.0", expected: "8.0.0", }, { name: "version with patch, no update", input: "8.0.100", expected: "8.0.100", }, { name: "version with patch and prerelease", input: "8.0.0-rc1", expected: "8.0.0-rc1", }, { name: "invalid update format, no update keyword", input: "8.0-foo302", expected: "8.0-foo302", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := convertNonCompliantSemver(tt.input) require.Equal(t, tt.expected, result) }) } } func TestJVMVersion_Invalid(t *testing.T) { tests := []struct { name string version string wantErr require.ErrorAssertionFunc }{ { name: "invalid version", version: "1.a", wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } _, err := newJvmVersion(tt.version) tt.wantErr(t, err) }) } } func TestJvmVersion_Compare_Formats(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.8.0_275", otherVersion: "1.8.0_281", otherFormat: JVMFormat, expectError: false, }, { name: "semantic format successful comparison", thisVersion: "1.8.0_275", otherVersion: "1.8.1", otherFormat: SemanticFormat, expectError: false, }, { name: "unknown format attempts upgrade to JVM - valid", thisVersion: "1.8.0_275", otherVersion: "1.8.0_281", otherFormat: UnknownFormat, expectError: false, }, { name: "unknown format attempts upgrade to Semantic - valid", thisVersion: "1.8.0_275", otherVersion: "1.9.0", otherFormat: UnknownFormat, expectError: false, }, { name: "unknown format fails all upgrades - invalid", thisVersion: "1.8.0_275", otherVersion: "not-valid-jvm-or-semver", otherFormat: UnknownFormat, expectError: true, errorSubstring: "invalid", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, err := newJvmVersion(test.thisVersion) require.NoError(t, err) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { require.ErrorContains(t, err, test.errorSubstring) } } else { require.NoError(t, err) require.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestJvmVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.8.0_275", JVMFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { require.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/kb_constraint.go ================================================ package version import "fmt" type kbConstraint struct { Raw string Expression simpleRangeExpression } func newKBConstraint(raw string) (kbConstraint, error) { if raw == "" { // an empty constraint is always satisfied return kbConstraint{}, nil } constraints, err := parseRangeExpression(raw) if err != nil { return kbConstraint{}, fmt.Errorf("unable to parse kb constraint phrase: %w", err) } return kbConstraint{ Raw: raw, Expression: constraints, }, nil } func (c kbConstraint) Satisfied(version *Version) (bool, error) { if c.Raw == "" { // an empty constraint is never satisfied return false, &NonFatalConstraintError{ constraint: c, version: version, message: "unexpected data in DB: empty raw version constraint", } } if version == nil { return true, nil } if version.Format != KBFormat { return false, newUnsupportedFormatError(KBFormat, version) } return c.Expression.satisfied(KBFormat, version) } func (c kbConstraint) Format() Format { return KBFormat } func (c kbConstraint) String() string { if c.Raw == "" { return fmt.Sprintf("%q (kb)", c.Raw) // with quotes } return fmt.Sprintf("%s (kb)", c.Raw) // no quotes } func (c kbConstraint) Value() string { return c.Raw } ================================================ FILE: grype/version/kb_constraint_test.go ================================================ package version import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestKbVersion_Constraint(t *testing.T) { tests := []testCase{ { name: "no constraint no version raises error", version: "", constraint: "", satisfied: false, wantError: func(t require.TestingT, err error, msgAndArgs ...interface{}) { var expectedError *NonFatalConstraintError assert.ErrorAs(t, err, &expectedError, "Unexpected error type from kbConstraint.Satisfied: %v", err) }, }, { name: "no constraint with version raises error", version: "878787", constraint: "", satisfied: false, wantError: func(t require.TestingT, err error, msgAndArgs ...interface{}) { var expectedError *NonFatalConstraintError assert.ErrorAs(t, err, &expectedError, "Unexpected error type from kbConstraint.Satisfied: %v", err) }, }, {name: "no version is unsatisfied", version: "", constraint: "foo", satisfied: false}, {name: "version constraint mismatch", version: "1", constraint: "foo", satisfied: false}, {name: "matching version and constraint", version: "1", constraint: "1", satisfied: true}, {name: "base keyword matching version and constraint", version: "base", constraint: "base", satisfied: true}, {name: "version and OR constraint match", version: "878787", constraint: "979797 || 101010 || 878787", satisfied: true}, {name: "version and OR constraint mismatch", version: "478787", constraint: "979797 || 101010 || 878787", satisfied: false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { constraint, err := GetConstraint(test.constraint, KBFormat) assert.NoError(t, err, "unexpected error from newKBConstraint: %v", err) test.assertVersionConstraint(t, KBFormat, constraint) }) } } ================================================ FILE: grype/version/kb_version.go ================================================ package version import ( "reflect" ) var _ Comparator = (*kbVersion)(nil) type kbVersion struct { version string } func newKBVersion(raw string) kbVersion { return kbVersion{ version: raw, } } func (v kbVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } return v.compare(newKBVersion(other.Raw)), nil } // compare returns 0 if v == v2, 1 otherwise func (v kbVersion) compare(other kbVersion) int { if reflect.DeepEqual(v, other) { return 0 } return 1 } func (v kbVersion) String() string { return v.version } ================================================ FILE: grype/version/kb_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestKbVersion_Compare(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "KB4562562", otherVersion: "KB4562563", otherFormat: KBFormat, expectError: false, }, { name: "different format does not return error", thisVersion: "KB4562562", otherVersion: "1.2.3", otherFormat: SemanticFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid kb format", thisVersion: "KB4562562", otherVersion: "KB4562563", otherFormat: UnknownFormat, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer := newKBVersion(test.thisVersion) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestKbVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { v := New("KB4562562", KBFormat) return v, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/maven_version.go ================================================ package version import ( "fmt" "regexp" mvnv "github.com/masahiro331/go-mvn-version" ) var _ Comparator = (*mavenVersion)(nil) // javaRuntimeQualifierPattern matches .jreNN or .jdkNN suffixes (case-insensitive) at the end of version strings var javaRuntimeQualifierPattern = regexp.MustCompile(`(?i)\.(jre|jdk)\d+$`) type mavenVersion struct { raw string obj mvnv.Version } // stripJavaRuntimeQualifier removes .jreNN or .jdkNN suffixes from version strings. // These are runtime-specific qualifiers that don't affect version comparison. // // The pattern matches 'jre' or 'jdk' (case-insensitive) followed by one or more digits // at the END of the version string only. This means: // - Case-insensitive: Both .jre11 and .JRE11 will be stripped // - Requires digits: .jre or .jdk without numbers will NOT be stripped // - End-anchored: .jre11-SNAPSHOT or .jdk17.beta will NOT be stripped // // Examples: // - "12.10.2.jre11" -> "12.10.2" (stripped) // - "12.10.2.JRE11" -> "12.10.2" (stripped) // - "12.10.2.jdk17" -> "12.10.2" (stripped) // - "12.10.2.JDK17" -> "12.10.2" (stripped) // - "12.10.2" -> "12.10.2" (no change) // - "12.10.2.jre" -> "12.10.2.jre" (no digits, not stripped) // - "12.10.2.jre11-SNAPSHOT" -> "12.10.2.jre11-SNAPSHOT" (not at end, not stripped) func stripJavaRuntimeQualifier(version string) string { return javaRuntimeQualifierPattern.ReplaceAllString(version, "") } func newMavenVersion(raw string) (mavenVersion, error) { // strip Java runtime qualifiers (e.g., .jre11, .jdk17) before parsing to ensure // versions like "12.10.2" and "12.10.2.jre11" are treated as equivalent for comparison. // The original raw version is preserved for display purposes. normalized := stripJavaRuntimeQualifier(raw) ver, err := mvnv.NewVersion(normalized) if err != nil { return mavenVersion{}, fmt.Errorf("could not generate new java version from: %s; %w", raw, err) } return mavenVersion{ raw: raw, obj: ver, }, nil } // Compare returns 0 if other == j, 1 if other > j, and -1 if other < j. // If an error is returned, the int value is -1 func (v mavenVersion) Compare(other *Version) (int, error) { if other == nil { return -1, fmt.Errorf("cannot compare nil version with %v", other) } o, err := newMavenVersion(other.Raw) if err != nil { return 0, err } return v.compare(o.obj) } func (v mavenVersion) compare(other mvnv.Version) (int, error) { if v.obj.Equal(other) { return 0, nil } if v.obj.LessThan(other) { return -1, nil } if v.obj.GreaterThan(other) { return 1, nil } return -1, fmt.Errorf( "could not compare java versions: %v with %v", other.String(), v.obj.String()) } ================================================ FILE: grype/version/maven_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStripJavaRuntimeQualifier(t *testing.T) { tests := []struct { name string input string want string }{ { name: "version with jre11", input: "12.10.2.jre11", want: "12.10.2", }, { name: "version with jdk17", input: "12.10.2.jdk17", want: "12.10.2", }, { name: "version with uppercase JRE11", input: "12.10.2.JRE11", want: "12.10.2", }, { name: "version with uppercase JDK17", input: "12.10.2.JDK17", want: "12.10.2", }, { name: "version with mixed case Jre11", input: "12.10.2.Jre11", want: "12.10.2", }, { name: "version without qualifier", input: "12.10.2", want: "12.10.2", }, { name: "version with jre but no digits", input: "12.10.2.jre", want: "12.10.2.jre", }, { name: "version with jdk but no digits", input: "12.10.2.jdk", want: "12.10.2.jdk", }, { name: "version with jre0 (zero)", input: "12.10.2.jre0", want: "12.10.2", }, { name: "version with jdk999 (large number)", input: "12.10.2.jdk999", want: "12.10.2", }, { name: "version with jre11 followed by SNAPSHOT", input: "12.10.2.jre11-SNAPSHOT", want: "12.10.2.jre11-SNAPSHOT", }, { name: "version with jdk17 followed by beta", input: "12.10.2.jdk17.beta", want: "12.10.2.jdk17.beta", }, { name: "version with JRE uppercase followed by SNAPSHOT", input: "12.10.2.JRE11-SNAPSHOT", want: "12.10.2.JRE11-SNAPSHOT", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := stripJavaRuntimeQualifier(tt.input) require.Equal(t, tt.want, got) }) } } func TestMavenVersion_Constraint(t *testing.T) { tests := []testCase{ // range expressions {version: "1", constraint: "< 2.5", satisfied: true}, {version: "1.0", constraint: "< 1.1", satisfied: true}, {version: "1.1", constraint: "< 1.2", satisfied: true}, {version: "1.0.0", constraint: "< 1.1", satisfied: true}, {version: "1.0.1", constraint: "< 1.1", satisfied: true}, {version: "1.1", constraint: "> 1.2.0", satisfied: false}, {version: "1.0-alpha-1", constraint: "> 1.0", satisfied: false}, {version: "1.0-alpha-1", constraint: "> 1.0-alpha-2", satisfied: false}, {version: "1.0-alpha-1", constraint: "< 1.0-beta-1", satisfied: true}, {version: "1.0-beta-1", constraint: "< 1.0-SNAPSHOT", satisfied: true}, {version: "1.0-SNAPSHOT", constraint: "< 1.0", satisfied: true}, {version: "1.0-alpha-1-SNAPSHOT", constraint: "> 1.0-alpha-1", satisfied: false}, {version: "1.0", constraint: "< 1.0-1", satisfied: true}, {version: "1.0-1", constraint: "< 1.0-2", satisfied: true}, {version: "1.0.0", constraint: "< 1.0-1", satisfied: true}, {version: "2.0-1", constraint: "> 2.0.1", satisfied: false}, {version: "2.0.1-klm", constraint: "> 2.0.1-lmn", satisfied: false}, {version: "2.0.1", constraint: "< 2.0.1-xyz", satisfied: true}, {version: "2.0.1", constraint: "< 2.0.1-123", satisfied: true}, {version: "2.0.1-xyz", constraint: "< 2.0.1-123", satisfied: true}, {version: "2.414.2-cb-5", constraint: "> 2.414.2", satisfied: true}, {version: "5.2.25.RELEASE", constraint: "< 5.2.25", satisfied: false}, {version: "5.2.25.RELEASE", constraint: "<= 5.2.25", satisfied: true}, // equality expressions {version: "1", constraint: "1", satisfied: true}, {version: "1", constraint: "1.0", satisfied: true}, {version: "1", constraint: "1.0.0", satisfied: true}, {version: "1.0", constraint: "1.0.0", satisfied: true}, {version: "1", constraint: "1-0", satisfied: true}, {version: "1", constraint: "1.0-0", satisfied: true}, {version: "1.0", constraint: "1.0-0", satisfied: true}, {version: "1a", constraint: "1-a", satisfied: true}, {version: "1a", constraint: "1.0-a", satisfied: true}, {version: "1a", constraint: "1.0.0-a", satisfied: true}, {version: "1.0a", constraint: "1-a", satisfied: true}, {version: "1.0.0a", constraint: "1-a", satisfied: true}, {version: "1x", constraint: "1-x", satisfied: true}, {version: "1x", constraint: "1.0-x", satisfied: true}, {version: "1x", constraint: "1.0.0-x", satisfied: true}, {version: "1.0x", constraint: "1-x", satisfied: true}, {version: "1.0.0x", constraint: "1-x", satisfied: true}, {version: "1ga", constraint: "1", satisfied: true}, {version: "1release", constraint: "1", satisfied: true}, {version: "1final", constraint: "1", satisfied: true}, {version: "1cr", constraint: "1rc", satisfied: true}, {version: "1a1", constraint: "1-alpha-1", satisfied: true}, {version: "1b2", constraint: "1-beta-2", satisfied: true}, {version: "1m3", constraint: "1-milestone-3", satisfied: true}, {version: "1X", constraint: "1x", satisfied: true}, {version: "1A", constraint: "1a", satisfied: true}, {version: "1B", constraint: "1b", satisfied: true}, {version: "1M", constraint: "1m", satisfied: true}, {version: "1Ga", constraint: "1", satisfied: true}, {version: "1GA", constraint: "1", satisfied: true}, {version: "1RELEASE", constraint: "1", satisfied: true}, {version: "1release", constraint: "1", satisfied: true}, {version: "1RELeaSE", constraint: "1", satisfied: true}, {version: "1Final", constraint: "1", satisfied: true}, {version: "1FinaL", constraint: "1", satisfied: true}, {version: "1FINAL", constraint: "1", satisfied: true}, {version: "1Cr", constraint: "1Rc", satisfied: true}, {version: "1cR", constraint: "1rC", satisfied: true}, {version: "1m3", constraint: "1Milestone3", satisfied: true}, {version: "1m3", constraint: "1MileStone3", satisfied: true}, {version: "1m3", constraint: "1MILESTONE3", satisfied: true}, {version: "1", constraint: "01", satisfied: true}, {version: "1", constraint: "001", satisfied: true}, {version: "1.1", constraint: "1.01", satisfied: true}, {version: "1.1", constraint: "1.001", satisfied: true}, {version: "1-1", constraint: "1-01", satisfied: true}, {version: "1-1", constraint: "1-001", satisfied: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { constraint, err := GetConstraint(test.constraint, MavenFormat) assert.NoError(t, err, "unexpected error from newMavenConstraint %s: %v", test.version, err) test.assertVersionConstraint(t, MavenFormat, constraint) }) } } func TestMavenVersion_Compare(t *testing.T) { tests := []struct { v1 string v2 string want int }{ { v1: "1", v2: "2", want: -1, }, { v1: "1.8.0_282", v2: "1.8.0_282", want: 0, }, { v1: "2.5", v2: "2.0", want: 1, }, { v1: "2.414.2-cb-5", v2: "2.414.2", want: 1, }, { v1: "5.2.25.RELEASE", // see https://mvnrepository.com/artifact/org.springframework/spring-web v2: "5.2.25", want: 0, }, { v1: "5.2.25.release", v2: "5.2.25", want: 0, }, { v1: "5.2.25.FINAL", v2: "5.2.25", want: 0, }, { v1: "5.2.25.final", v2: "5.2.25", want: 0, }, { v1: "5.2.25.GA", v2: "5.2.25", want: 0, }, { v1: "5.2.25.ga", v2: "5.2.25", want: 0, }, // JRE/JDK qualifier tests (GitHub issue: JRE version matching) { v1: "12.10.2", v2: "12.10.2.jre11", want: 0, }, { v1: "12.10.2.jre11", v2: "12.10.2", want: 0, }, { v1: "12.10.2.jdk17", v2: "12.10.2", want: 0, }, { v1: "12.10.2.jre11", v2: "12.10.2.jdk17", want: 0, }, { v1: "12.10.1", v2: "12.10.2.jre11", want: -1, }, { v1: "12.10.2.jre11", v2: "12.10.1", want: 1, }, { v1: "1.2.3.jre8", v2: "1.2.4.jre8", want: -1, }, } for _, tt := range tests { t.Run(tt.v1+" vs "+tt.v2, func(t *testing.T) { v1 := New(tt.v1, MavenFormat) v2 := New(tt.v2, MavenFormat) if got, _ := v1.Compare(v2); got != tt.want { t.Errorf("Compare() = %v, want %v", got, tt.want) } }) } } func TestMavenVersion_Compare_Format(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: MavenFormat, expectError: false, }, { name: "same format successful comparison with qualifiers", thisVersion: "1.2.3-SNAPSHOT", otherVersion: "1.2.3-RELEASE", otherFormat: MavenFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid maven format", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: UnknownFormat, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer := New(test.thisVersion, MavenFormat) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestMavenVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3", MavenFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, { name: "incomparable maven versions", setupFunc: func(t testing.TB) (*Version, *Version) { // This test would be hard to construct in practice since the Maven // version library handles most comparisons, but we can simulate the // error condition by creating a mock that would trigger the last // error condition in the Compare function thisVer := New("1.2.3", MavenFormat) // We'd need to modify the otherVer manually to create a scenario // where none of the comparison methods return true, which is unlikely // in real usage but could be simulated for test coverage otherVer := New("1.2.4", MavenFormat) return thisVer, otherVer }, expectError: false, // Changed to false since we can't easily trigger the last error condition errorSubstring: "could not compare java versions", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) } }) } } ================================================ FILE: grype/version/operator.go ================================================ package version import "fmt" const ( EQ Operator = "=" GT Operator = ">" LT Operator = "<" GTE Operator = ">=" LTE Operator = "<=" ) type Operator string func parseOperator(op string) (Operator, error) { switch op { case string(EQ), "": return EQ, nil case string(GT): return GT, nil case string(GTE): return GTE, nil case string(LT): return LT, nil case string(LTE): return LTE, nil } return "", fmt.Errorf("unknown operator: '%s'", op) } ================================================ FILE: grype/version/pacman_version.go ================================================ package version import ( "fmt" "reflect" "strings" "unicode" ) var _ Comparator = (*pacmanVersion)(nil) type pacmanVersion struct { epoch *int version string release string } func newPacmanVersion(raw string) (pacmanVersion, error) { epoch, remainingVersion, err := splitEpochFromVersion(raw) if err != nil { return pacmanVersion{}, err } fields := strings.SplitN(remainingVersion, "-", 2) version := fields[0] var release string if len(fields) > 1 { // there is a release release = fields[1] } return pacmanVersion{ epoch: epoch, version: version, release: release, }, nil } func (v pacmanVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newPacmanVersion(other.Raw) if err != nil { return 0, err } return v.compare(o), nil } // Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2. // Pacman uses a similar scheme to RPM: epoch:version-release // If epochs are NOT present and explicit in both versions then they are ignored for the comparison. func (v pacmanVersion) compare(v2 pacmanVersion) int { if reflect.DeepEqual(v, v2) { return 0 } // Only compare epochs if both are present and explicit if epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) { epochResult := compareEpochs(*v.epoch, *v2.epoch) if epochResult != 0 { return epochResult } } ret := comparePacmanVersions(v.version, v2.version) if ret != 0 { return ret } return comparePacmanVersions(v.release, v2.release) } func (v pacmanVersion) String() string { version := "" if v.epoch != nil { version += fmt.Sprintf("%d:", *v.epoch) } version += v.version if v.release != "" { version += fmt.Sprintf("-%s", v.release) } return version } // comparePacmanVersions compares two version or release strings without the epoch. // Pacman version comparison is similar to RPM, comparing alphanumeric segments. // Source: https://wiki.archlinux.org/title/Pacman/Tips_and_tricks#Version_comparison // The scheme is based on RPM's algorithm. // // Note: dupl lint is suppressed because although pacman's vercmp is based on rpm's vercmp, // they are not identical and may diverge in the future. We intentionally keep them decoupled. // //nolint:funlen,gocognit,dupl func comparePacmanVersions(a, b string) int { // shortcut for equality if a == b { return 0 } // get alpha/numeric segments segsa := alphanumPattern.FindAllString(a, -1) segsb := alphanumPattern.FindAllString(b, -1) maxSegs := max(len(segsa), len(segsb)) minSegs := min(len(segsa), len(segsb)) // compare each segment for i := 0; i < minSegs; i++ { a := segsa[i] b := segsb[i] // compare tildes if []rune(a)[0] == '~' || []rune(b)[0] == '~' { if []rune(a)[0] != '~' { return 1 } if []rune(b)[0] != '~' { return -1 } } if unicode.IsNumber([]rune(a)[0]) { // numbers are always greater than alphas if !unicode.IsNumber([]rune(b)[0]) { // a is numeric, b is alpha return 1 } // trim leading zeros a = strings.TrimLeft(a, "0") b = strings.TrimLeft(b, "0") // longest string wins without further comparison if len(a) > len(b) { return 1 } else if len(b) > len(a) { return -1 } } else if unicode.IsNumber([]rune(b)[0]) { // a is alpha, b is numeric return -1 } // string compare if a < b { return -1 } else if a > b { return 1 } } // segments were all the same but separators must have been different if len(segsa) == len(segsb) { return 0 } // If there is a tilde in a segment past the min number of segments, find it. if len(segsa) > minSegs && []rune(segsa[minSegs])[0] == '~' { return -1 } else if len(segsb) > minSegs && []rune(segsb[minSegs])[0] == '~' { return 1 } // are the remaining segments 0s? segaAll0s := true segbAll0s := true for i := minSegs; i < maxSegs; i++ { if i < len(segsa) && segsa[i] != "0" { segaAll0s = false } if i < len(segsb) && segsb[i] != "0" { segbAll0s = false } } if segaAll0s && segbAll0s { return 0 } // whoever has the most segments wins if len(segsa) > len(segsb) { return 1 } return -1 } ================================================ FILE: grype/version/pacman_version_test.go ================================================ package version import ( "testing" "github.com/stretchr/testify/assert" ) func TestPacmanVersionCompare(t *testing.T) { tests := []struct { name string v1 string v2 string want int wantErr bool }{ { name: "equal versions", v1: "1.0.0", v2: "1.0.0", want: 0, wantErr: false, }, { name: "first greater", v1: "1.0.1", v2: "1.0.0", want: 1, wantErr: false, }, { name: "second greater", v1: "1.0.0", v2: "1.0.1", want: -1, wantErr: false, }, { name: "with release numbers", v1: "1.0.0-1", v2: "1.0.0-2", want: -1, wantErr: false, }, { name: "with release numbers greater", v1: "1.0.0-2", v2: "1.0.0-1", want: 1, wantErr: false, }, { name: "complex version", v1: "5.6.0-1", v2: "5.6.0-2", want: -1, wantErr: false, }, { name: "alpha vs release", v1: "1.0.0alpha", v2: "1.0.0", want: 1, wantErr: false, }, { name: "with epoch", v1: "1:1.0.0", v2: "2:1.0.0", want: -1, wantErr: false, }, { name: "epoch takes precedence", v1: "2:1.0.0", v2: "1:2.0.0", want: 1, wantErr: false, }, { name: "tilde version", v1: "1.0.0~rc1", v2: "1.0.0", want: -1, wantErr: false, }, { name: "leading zeros", v1: "1.0.001", v2: "1.0.1", want: 0, wantErr: false, }, { name: "version with plus sign", v1: "0.115+24+g5230646-1", v2: "0.116-1", want: -1, wantErr: false, }, { name: "version with git hash suffix", v1: "0.12.8+8+ga957a90b-1", v2: "0.12.8+8+ga957a90b-2", want: -1, wantErr: false, }, { name: "real arch versions curl", v1: "8.4.0-1", v2: "8.5.0-1", want: -1, wantErr: false, }, { name: "real arch versions openssl with epoch", v1: "1:3.0.7-4", v2: "1:3.0.8-1", want: -1, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v1, err := newPacmanVersion(tt.v1) assert.NoError(t, err) v2 := New(tt.v2, PacmanFormat) result, err := v1.Compare(v2) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.want, result) } }) } } func TestPacmanVersionString(t *testing.T) { tests := []struct { name string raw string want string }{ { name: "simple version", raw: "1.0.0", want: "1.0.0", }, { name: "with release", raw: "1.0.0-1", want: "1.0.0-1", }, { name: "with epoch", raw: "1:1.0.0", want: "1:1.0.0", }, { name: "with epoch and release", raw: "1:1.0.0-1", want: "1:1.0.0-1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v, err := newPacmanVersion(tt.raw) assert.NoError(t, err) assert.Equal(t, tt.want, v.String()) }) } } ================================================ FILE: grype/version/pep440_version.go ================================================ package version import ( goPepVersion "github.com/aquasecurity/go-pep440-version" ) var _ Comparator = (*pep440Version)(nil) type pep440Version struct { // public is the public portion of the version (without local segment), used for most comparisons public goPepVersion.Version // full is the complete parsed version including local segment, used when constraint has local full goPepVersion.Version } func newPep440Version(raw string) (pep440Version, error) { // lets ensure this is a valid PEP 440 version parsed, err := goPepVersion.Parse(raw) if err != nil { return pep440Version{}, invalidFormatError(SemanticFormat, raw, err) } // we want to use the "public" portion of the version for comparison purposes (for specifier matching, not local versions). // Note per PEP 440: // [+] // see: // - https://peps.python.org/pep-0440/#public-version-identifiers // - https://peps.python.org/pep-0440/#local-version-identifiers // // This means that for a version like "1.0.0+abc.1", we only want to consider "1.0.0" for comparison purposes. public, err := goPepVersion.Parse(parsed.Public()) if err != nil { return pep440Version{}, invalidFormatError(SemanticFormat, raw, err) } return pep440Version{ public: public, full: parsed, }, nil } func (v pep440Version) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newPep440Version(other.Raw) if err != nil { return 0, err } result := v.public.Compare(o.public) if result != 0 { return result, nil } // Public portions are equal - handle local version segments per PEP 440 specifier semantics. // If constraint has no local segment, ignore package's local (they're equal). // If constraint has a local segment, require exact match. if o.full.Local() == "" { return 0, nil } return v.full.Compare(o.full), nil } ================================================ FILE: grype/version/pep440_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPep440Version_Constraint(t *testing.T) { tests := []testCase{ { name: "empty constraint", version: "2.3.1", constraint: "", satisfied: true, }, { name: "version range within", constraint: ">1.0, <2.0", version: "1.2+beta-3", satisfied: true, }, { name: "version within compound range", constraint: ">1.0, <2.0 || > 3.0", version: "3.2+beta-3", satisfied: true, }, { name: "version within compound range (2)", constraint: ">1.0, <2.0 || > 3.0", version: "1.2+beta-3", satisfied: true, }, { name: "version not within compound range", constraint: ">1.0, <2.0 || > 3.0", version: "2.2+beta-3", satisfied: false, }, { name: "version range outside (right)", constraint: ">1.0, <2.0", version: "2.1-beta-3", satisfied: false, }, { name: "version range outside (left)", constraint: ">1.0, <2.0", version: "0.9-beta-2", satisfied: false, }, { name: "version range within (excluding left, prerelease)", constraint: ">=1.0, <2.0", version: "1.0-beta-3", satisfied: false, }, { name: "version range within (including left)", constraint: ">=1.1, <2.0", version: "1.1", satisfied: true, }, { name: "version range within (excluding right, 1)", constraint: ">1.0, <=2.0", version: "2.0-beta-3", satisfied: true, }, { name: "version range within (excluding right, 2)", constraint: ">1.0, <2.0", version: "2.0-beta-3", satisfied: true, }, { name: "version range within (including right)", constraint: ">1.0, <=2.0", version: "2.0", satisfied: true, }, { name: "version range within (including right, longer version [valid semver, bad fuzzy])", constraint: ">1.0, <=2.0", version: "2.0.0", satisfied: true, }, { name: "bad semver (eq)", version: "5a2", constraint: "=5a2", satisfied: true, }, { name: "bad semver (gt)", version: "5a2", constraint: ">5a1", satisfied: true, }, { name: "bad semver (lt)", version: "5a2", constraint: "<6a1", satisfied: true, }, { name: "bad semver (lte)", version: "5a2", constraint: "<=5a2", satisfied: true, }, { name: "bad semver (gte)", version: "5a2", constraint: ">=5a2", satisfied: true, }, { name: "bad semver (lt boundary)", version: "5a2", constraint: "<5a2", satisfied: false, }, // regression for https://github.com/anchore/go-version/pull/2 { name: "indirect package match", version: "1.3.2-r0", constraint: "<= 1.3.3-r0", satisfied: true, }, { name: "indirect package no match", version: "1.3.4-r0", constraint: "<= 1.3.3-r0", satisfied: false, }, { name: "vulndb fuzzy constraint single quoted", version: "4.5.2", constraint: "'4.5.1' || '4.5.2'", satisfied: true, }, { name: "vulndb fuzzy constraint double quoted", version: "4.5.2", constraint: "\"4.5.1\" || \"4.5.2\"", satisfied: true, }, { name: "rc candidates with no '-' can match semver pattern", version: "1.20rc1", constraint: " = 1.20.0-rc1", satisfied: true, }, { name: "candidates ahead of alpha", version: "3.11.0", constraint: "> 3.11.0-alpha1", satisfied: true, }, { name: "candidates ahead of beta", version: "3.11.0", constraint: "> 3.11.0-beta1", satisfied: true, }, { name: "candidates ahead of same alpha versions", version: "3.11.0-alpha5", constraint: "> 3.11.0-alpha1", satisfied: true, }, { name: "candidates are placed correctly between alpha and release", version: "3.11.0-beta5", constraint: "3.11.0 || = 3.11.0-alpha1", satisfied: false, }, { name: "candidates with pre suffix are sorted numerically", version: "1.0.2pre1", constraint: " < 1.0.2pre2", satisfied: true, }, { name: "openssl pre2 is still considered less than release", version: "1.1.1-pre2", constraint: "> 1.1.1-pre1, < 1.1.1", satisfied: true, }, { name: "major version releases are less than their subsequent patch releases with letter suffixes", version: "1.1.1", constraint: "> 1.1.1-a", satisfied: true, }, { name: "date based pep440 version string boundary condition", version: "2022.12.7", constraint: ">=2017.11.05,<2022.12.07", }, { name: "certifi false positive is fixed", version: "2022.12.7", constraint: ">=2017.11.05,<2022.12.07", }, // regression (partial version with metadata should be valid) // this is a fun one! PEP 440 has two different use cases for ordering semantics: direct versions and version specifiers. // Take this python code for example: // // ```python // from packaging.version import Version // from packaging.specifiers import SpecifierSet // // # direct ordering comparison // Version('6.4+cgr.1') <= Version('6.4.0') # False // // # specifier matching // Version('6.4+cgr.1') in SpecifierSet('<=6.4.0') # True // ``` // // The root cause of the regression is that we have been doing direct version comparisons instead of specifier matching. // The fix is to treat constraint matching as specifier matching (only consider the public version segment for // constraint matching, not the local version segment). // // We want specifier semantics (ignore local) since 6.4+cgr.1 should be considered "the same release" as // 6.4 for vulnerability matching applicability. { name: "partial version with metadata", version: "6.4+cgr.1", constraint: "<=6.4.0", satisfied: true, }, // When constraint has a local version, require exact match (important for unaffected entries) { name: "local version in constraint should not match version without local segment", version: "2.0.0", constraint: "= 2.0.0+cgr.1", satisfied: false, }, { name: "local version in constraint should match same local version", version: "2.0.0+cgr.1", constraint: "= 2.0.0+cgr.1", satisfied: true, }, { name: "version with local segment should match constraint without local segment", version: "2.0.0+cgr.1", constraint: "= 2.0.0", satisfied: true, }, { name: "version with local segment should satisfy less-than constraint", version: "2.0.0+cgr.1", constraint: "< 2.0.1", satisfied: true, }, { name: "different local versions should not match on equality", version: "2.0.0+other", constraint: "= 2.0.0+cgr.1", satisfied: false, }, // Local version segments compared per PEP 440 (numeric segments as integers) { name: "local version segments compared numerically not lexicographically", version: "2.0.0+cgr.12", constraint: "> 2.0.0+cgr.2", satisfied: true, }, { name: "local version segment numeric comparison - less than", version: "2.0.0+cgr.2", constraint: "< 2.0.0+cgr.12", satisfied: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { c, err := GetConstraint(tc.constraint, PythonFormat) require.NoError(t, err) v := New(tc.version, PythonFormat) sat, err := c.Satisfied(v) require.NoError(t, err) assert.Equal(t, tc.satisfied, sat) }) } } func TestPep440Version_Compare(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: PythonFormat, expectError: false, }, { name: "same format successful comparison with pre-release", thisVersion: "1.2.3a1", otherVersion: "1.2.3b2", otherFormat: PythonFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid python format", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: UnknownFormat, expectError: false, }, { name: "unknown format attempts upgrade - invalid python format", thisVersion: "1.2.3", otherVersion: "not/valid/python-format", otherFormat: UnknownFormat, expectError: true, errorSubstring: "invalid", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, err := newPep440Version(test.thisVersion) require.NoError(t, err) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestPep440Version_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3", PythonFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/portage_version.go ================================================ package version import ( "math/big" "regexp" "strings" ) var _ Comparator = (*portageVersion)(nil) // for the original python implementation, see: // https://github.com/gentoo/portage/blob/master/lib/portage/versions.py var ( versionRegexp = regexp.MustCompile(`(\d+)((\.\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\d*)*)(-r(\d+))?`) suffixRegexp = regexp.MustCompile(`^(alpha|beta|rc|pre|p)(\d*)$`) suffixValue = map[string]int{"pre": -2, "p": 0, "alpha": -4, "beta": -3, "rc": -1} ) type portageVersion struct { version string } func newPortageVersion(raw string) portageVersion { return portageVersion{ version: raw, } } func (v portageVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } return v.compare(newPortageVersion(other.Raw)), nil } // Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2. func (v portageVersion) compare(v2 portageVersion) int { if v.version == v2.version { return 0 } return comparePortageVersions(v.version, v2.version) } //nolint:funlen,gocognit func comparePortageVersions(a, b string) int { match1 := versionRegexp.FindStringSubmatch(a) match2 := versionRegexp.FindStringSubmatch(b) list1 := []*big.Int{big.NewInt(0)} list2 := []*big.Int{big.NewInt(0)} list1[0].SetString(match1[1], 10) list2[0].SetString(match2[1], 10) vlist1 := strings.Split(match1[2], ".")[1:] vlist2 := strings.Split(match2[2], ".")[1:] vlistMaxLen := len(vlist1) if len(vlist2) > vlistMaxLen { vlistMaxLen = len(vlist2) } for index := 0; index < vlistMaxLen; index++ { switch { case len(vlist1) <= index: list1 = append(list1, big.NewInt(-1)) i := big.NewInt(0) i.SetString(vlist2[index], 10) list2 = append(list2, i) case len(vlist2) <= index: list2 = append(list2, big.NewInt(-1)) i := big.NewInt(0) i.SetString(vlist1[index], 10) list1 = append(list1, i) case !strings.HasPrefix(vlist1[index], "0") && !strings.HasPrefix(vlist2[index], "0"): i := big.NewInt(0) i.SetString(vlist1[index], 10) list1 = append(list1, i) j := big.NewInt(0) j.SetString(vlist2[index], 10) list2 = append(list2, j) default: maxLen := len(vlist1[index]) if len(vlist2[index]) > maxLen { maxLen = len(vlist2[index]) } if len(vlist1[index]) < maxLen { vlist1[index] += strings.Repeat("0", maxLen-len(vlist1[index])) } if len(vlist2[index]) < maxLen { vlist2[index] += strings.Repeat("0", maxLen-len(vlist2[index])) } i := big.NewInt(0) i.SetString(vlist1[index], 10) list1 = append(list1, i) j := big.NewInt(0) j.SetString(vlist2[index], 10) list2 = append(list2, j) } } if len(match1[4]) != 0 { r := []rune(match1[4]) i := big.NewInt(int64(r[0])) list1 = append(list1, i) } if len(match2[4]) != 0 { r := []rune(match2[4]) i := big.NewInt(int64(r[0])) list2 = append(list2, i) } maxLen := len(list1) if len(list2) > maxLen { maxLen = len(list2) } for index := 0; index < maxLen; index++ { if len(list1) <= index { return -1 } if len(list2) <= index { return 1 } c := list1[index].Cmp(list2[index]) if c != 0 { return c } } slist1 := strings.Split(match1[5], "_")[1:] slist2 := strings.Split(match2[5], "_")[1:] maxLen = len(slist1) if len(slist2) > maxLen { maxLen = len(slist2) } for index := 0; index < maxLen; index++ { s1 := []string{"p", "-1"} s2 := []string{"p", "-1"} if len(slist1) > index { s1 = suffixRegexp.FindStringSubmatch(slist1[index])[1:] if s1[1] == "" { s1[1] = "0" } } if len(slist2) > index { s2 = suffixRegexp.FindStringSubmatch(slist2[index])[1:] if s2[1] == "" { s2[1] = "0" } } if s1[0] != s2[0] { v1 := suffixValue[s1[0]] v2 := suffixValue[s2[0]] if v1 > v2 { return 1 } return -1 } if s1[1] != s2[1] { i := big.NewInt(0) i.SetString(s1[1], 10) j := big.NewInt(0) j.SetString(s2[1], 10) c := i.Cmp(j) if c != 0 { return c } } } r1 := big.NewInt(0) if match1[9] != "" { r1.SetString(match1[9], 10) } r2 := big.NewInt(0) if match2[9] != "" { r2.SetString(match2[9], 10) } return r1.Cmp(r2) } ================================================ FILE: grype/version/portage_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPortageVersion_Constraint(t *testing.T) { tests := []testCase{ // empty constraint is always satisfied {version: "1.2.3", constraint: "", satisfied: true}, {version: "1.2.3-r1", constraint: "", satisfied: true}, {version: "1.2.3_alpha1", constraint: "", satisfied: true}, // simple equality {version: "1.2.3", constraint: "= 1.2.3", satisfied: true}, {version: "1.2.3-r1", constraint: "= 1.2.3-r1", satisfied: true}, {version: "1.2.3", constraint: "= 1.2.4", satisfied: false}, // less than {version: "1.2.3", constraint: "< 1.2.4", satisfied: true}, {version: "1.2.3", constraint: "< 1.2.3", satisfied: false}, {version: "1.2.3", constraint: "< 1.2.2", satisfied: false}, {version: "1.2.3-r1", constraint: "< 1.2.3-r2", satisfied: true}, {version: "1.2.3-r2", constraint: "< 1.2.3-r1", satisfied: false}, // less than or equal {version: "1.2.3", constraint: "<= 1.2.3", satisfied: true}, {version: "1.2.3", constraint: "<= 1.2.4", satisfied: true}, {version: "1.2.3", constraint: "<= 1.2.2", satisfied: false}, {version: "1.2.3-r1", constraint: "<= 1.2.3-r1", satisfied: true}, // greater than {version: "1.2.4", constraint: "> 1.2.3", satisfied: true}, {version: "1.2.3", constraint: "> 1.2.3", satisfied: false}, {version: "1.2.2", constraint: "> 1.2.3", satisfied: false}, {version: "1.2.3-r2", constraint: "> 1.2.3-r1", satisfied: true}, {version: "1.2.3-r1", constraint: "> 1.2.3-r2", satisfied: false}, // greater than or equal {version: "1.2.3", constraint: ">= 1.2.3", satisfied: true}, {version: "1.2.4", constraint: ">= 1.2.3", satisfied: true}, {version: "1.2.2", constraint: ">= 1.2.3", satisfied: false}, {version: "1.2.3-r1", constraint: ">= 1.2.3-r1", satisfied: true}, // compound conditions with AND (comma) {version: "1.5.0", constraint: "> 1.0.0, < 2.0.0", satisfied: true}, {version: "0.5.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.2.3-r5", constraint: ">= 1.2.3-r1, <= 1.2.3-r10", satisfied: true}, // compound conditions with OR {version: "0.5.0", constraint: "< 1.0.0 || > 2.0.0", satisfied: true}, {version: "3.0.0", constraint: "< 1.0.0 || > 2.0.0", satisfied: true}, {version: "1.5.0", constraint: "< 1.0.0 || > 2.0.0", satisfied: false}, // complex compound conditions {version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.3.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.8.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, // portage-specific version features // letter suffixes (a, b, c etc.) {version: "1.2a", constraint: "< 1.2b", satisfied: true}, {version: "1.2b", constraint: "< 1.2a", satisfied: false}, {version: "12.2.5", constraint: "> 12.2b", satisfied: true}, // revision numbers (-r suffix) {version: "1.0.0-r1", constraint: "> 1.0.0", satisfied: true}, {version: "1.0.0", constraint: "> 1.0.0-r1", satisfied: false}, {version: "1.2.3-r2", constraint: "> 1.2.3-r1", satisfied: true}, {version: "1.2.3-r1", constraint: "< 1.2.3-r2", satisfied: true}, // version suffixes (alpha, beta, pre, rc, p) {version: "1.0.0_alpha1", constraint: "< 1.0.0_beta1", satisfied: true}, {version: "1.0.0_beta1", constraint: "< 1.0.0_rc1", satisfied: true}, {version: "1.0.0_rc1", constraint: "< 1.0.0", satisfied: true}, {version: "1.0.0", constraint: "< 1.0.0_p1", satisfied: true}, {version: "1.0.0_pre1", constraint: "> 1.0.0_alpha1", satisfied: true}, // patch level suffixes {version: "1_p1", constraint: "> 1_p0", satisfied: true}, {version: "1_p0", constraint: "> 1", satisfied: true}, // decimal versions with leading zeros {version: "1.01", constraint: "< 1.1", satisfied: true}, {version: "1.1", constraint: "> 1.01", satisfied: true}, // version with missing patch components {version: "12.2", constraint: "< 12.2.0", satisfied: true}, // 12.2 < 12.2.0 is true in portage 🤯 {version: "12.2.0", constraint: "> 12.2", satisfied: true}, // edge cases - versions that should not match {version: "1.2.3", constraint: "= 1.2.4", satisfied: false}, {version: "1.2.3", constraint: "> 1.2.3", satisfied: false}, {version: "1.2.3", constraint: "< 1.2.3", satisfied: false}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, PortageFormat) assert.NoError(t, err) test.assertVersionConstraint(t, PortageFormat, constraint) }) } } func TestPortageConstraint_Constraint_NilVersion(t *testing.T) { tests := []struct { name string constraint string expected bool shouldError bool }{ { name: "empty constraint with nil version", constraint: "", expected: true, shouldError: false, }, { name: "non-empty constraint with nil version", constraint: "> 1.0.0", expected: false, shouldError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { c, err := GetConstraint(test.constraint, PortageFormat) assert.NoError(t, err) satisfied, err := c.Satisfied(nil) if test.shouldError { require.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, test.expected, satisfied) } }) } } func TestPortageVersion_Constraint_UnsupportedFormat(t *testing.T) { c, err := GetConstraint("> 1.0.0", PortageFormat) assert.NoError(t, err) // test with a semantic version (wrong format) version := New("1.2.3", SemanticFormat) satisfied, err := c.Satisfied(version) require.Error(t, err) assert.False(t, satisfied) assert.Contains(t, err.Error(), "unsupported version comparison") } func TestPortageConstraint_String(t *testing.T) { tests := []struct { name string constraint string expected string }{ { name: "empty constraint", constraint: "", expected: "none (portage)", }, { name: "simple constraint", constraint: "> 1.0.0", expected: "> 1.0.0 (portage)", }, { name: "complex constraint", constraint: "> 1.0.0, < 2.0.0", expected: "> 1.0.0, < 2.0.0 (portage)", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { constraint, err := GetConstraint(test.constraint, PortageFormat) assert.NoError(t, err) result := constraint.String() assert.Equal(t, test.expected, result) }) } } func TestPortageVersion_Compare(t *testing.T) { tests := []struct { v1 string v2 string result int }{ {"1", "1", 0}, {"12.2.5", "12.2b", 1}, {"12.2a", "12.2b", -1}, {"12.2", "12.2.0", -1}, {"1.01", "1.1", -1}, {"1_p1", "1_p0", 1}, {"1_p0", "1", 1}, {"1-r1", "1", 1}, {"1.2.3-r2", "1.2.3-r1", 1}, {"1.2.3-r1", "1.2.3-r2", -1}, } for _, test := range tests { name := test.v1 + "_vs_" + test.v2 t.Run(name, func(t *testing.T) { v1 := New(test.v1, PortageFormat) v2 := New(test.v2, PortageFormat) actual, err := v1.Compare(v2) require.NoError(t, err) assert.Equal(t, test.result, actual, "expected comparison result to match") }) } } func TestPortageVersion_Compare_Format(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: PortageFormat, expectError: false, }, { name: "same format successful comparison with suffixes", thisVersion: "1.2.3-r1", otherVersion: "1.2.3-r2", otherFormat: PortageFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid portage format", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: UnknownFormat, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer := New(test.thisVersion, PortageFormat) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestPortageVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3", PortageFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/range.go ================================================ package version import ( "fmt" "regexp" "strconv" "strings" "github.com/anchore/grype/internal/stringutil" ) // Operator group only matches on range operators (GT, LT, GTE, LTE, E) // version group matches on everything except for whitespace and operators (range or boolean) var constraintPartPattern = regexp.MustCompile(`\s*(?P[^><=a-zA-Z0-9().'"]*)(?P[><=]*)\s*(?P.+)`) type rangeUnit struct { Operator Operator Version string } func parseRange(phrase string) (*rangeUnit, error) { match := stringutil.MatchCaptureGroups(constraintPartPattern, phrase) version, exists := match["version"] if !exists { return nil, nil } opStr := match["operator"] prefix := match["prefix"] if prefix != "" && opStr == "" { return nil, fmt.Errorf("constraint has an unprocessable prefix %q", prefix) } version = strings.Trim(version, " ") if err := validateVersion(version); err != nil { return nil, err } // version may have quotes, attempt to unquote it (ignore errors) unquoted, err := trimQuotes(version) if err == nil { version = unquoted } op, err := parseOperator(opStr) if err != nil { return nil, fmt.Errorf("unable to parse constraint operator=%q: %+v", opStr, err) } return &rangeUnit{ Operator: op, Version: version, }, nil } // trimQuotes will attempt to remove double quotes. // If removing double quotes is unsuccessful, it will attempt to remove single quotes. // If neither operation is successful, it will return an error. func trimQuotes(s string) (string, error) { unquoted, err := strconv.Unquote(s) switch { case err == nil: return unquoted, nil case strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'"): return strings.Trim(s, "'"), nil default: return s, fmt.Errorf("string %s is not single or double quoted", s) } } func (c *rangeUnit) Satisfied(comparison int) bool { switch c.Operator { case EQ: return comparison == 0 case GT: return comparison > 0 case GTE: return comparison >= 0 case LT: return comparison < 0 case LTE: return comparison <= 0 default: panic(fmt.Errorf("unknown operator: %s", c.Operator)) } } // validateVersion scans the version string and validates characters outside of quotes. // invalid characters within quotes are allowed, but unbalanced quotes are not allowed. func validateVersion(version string) error { var inQuotes bool var quoteChar rune for _, r := range version { switch { case !inQuotes && (r == '"' || r == '\''): // start of quoted section inQuotes = true quoteChar = r case inQuotes && r == quoteChar: // end of quoted section inQuotes = false quoteChar = 0 case !inQuotes && strings.ContainsRune("><=", r): // invalid character outside of quotes return fmt.Errorf("version %q potentially is a version constraint expression (should not contain '><=' outside of quotes)", version) } } if inQuotes { return fmt.Errorf("version %q has unbalanced quotes", version) } return nil } ================================================ FILE: grype/version/range_expression.go ================================================ package version import ( "bytes" "fmt" "strings" "text/scanner" ) type simpleRangeExpression struct { Units [][]rangeUnit // only supports or'ing a group of and'ed groups } func parseRangeExpression(phrase string) (simpleRangeExpression, error) { orParts, err := scanExpression(phrase) if err != nil { return simpleRangeExpression{}, fmt.Errorf("unable to create constraint expression from=%q : %w", phrase, err) } orUnits := make([][]rangeUnit, len(orParts)) var fuzzyErr error for orIdx, andParts := range orParts { andUnits := make([]rangeUnit, len(andParts)) for andIdx, part := range andParts { unit, err := parseRange(part) if err != nil { return simpleRangeExpression{}, err } if unit == nil { return simpleRangeExpression{}, fmt.Errorf("unable to parse unit: %q", part) } andUnits[andIdx] = *unit } orUnits[orIdx] = andUnits } return simpleRangeExpression{ Units: orUnits, }, fuzzyErr } func (c *simpleRangeExpression) satisfied(format Format, version *Version) (bool, error) { // Use the version's embedded config if present, otherwise use empty config cfg := ComparisonConfig{} if version != nil { cfg = version.Config } return c.satisfiedWithConfig(format, version, cfg) } func (c *simpleRangeExpression) satisfiedWithConfig(format Format, version *Version, cfg ComparisonConfig) (bool, error) { oneSatisfied := false for i, andOperand := range c.Units { allSatisfied := true for j, andUnit := range andOperand { constraintVersion := &Version{ Format: format, Raw: andUnit.Version, } var result int var err error // Use config-aware comparison for RPM and Deb formats when config is provided if cfg.MissingEpochStrategy != "" && (format == RpmFormat || format == DebFormat) { result, err = compareWithConfig(version, constraintVersion, cfg) } else { result, err = version.Compare(constraintVersion) } if err != nil { return false, fmt.Errorf("uncomparable %T vs %q: %w", andUnit, version.String(), err) } unit := c.Units[i][j] if !unit.Satisfied(result) { allSatisfied = false } } oneSatisfied = oneSatisfied || allSatisfied } return oneSatisfied, nil } // compareWithConfig performs a version comparison using the provided configuration. // This function extracts the comparator and calls CompareWithConfig if the comparator // supports it (RPM and Deb versions). func compareWithConfig(version *Version, constraintVersion *Version, cfg ComparisonConfig) (int, error) { comparator, err := version.getComparator(version.Format) if err != nil { return 0, err } // Check if the comparator supports config-aware comparison switch v := comparator.(type) { case rpmVersion: return v.CompareWithConfig(constraintVersion, cfg) case debVersion: return v.CompareWithConfig(constraintVersion, cfg) default: // Fall back to regular comparison for other formats return comparator.Compare(constraintVersion) } } func scanExpression(phrase string) ([][]string, error) { var scnr scanner.Scanner var orGroups [][]string // all versions a group of and'd groups or'd together var andGroup []string // most current group of and'd versions var buf bytes.Buffer // most current single version value var lastToken string captureVersionOperatorPair := func() { if buf.Len() > 0 { ver := buf.String() andGroup = append(andGroup, ver) buf.Reset() } } captureAndGroup := func() { if len(andGroup) > 0 { orGroups = append(orGroups, andGroup) andGroup = nil } } scnr.Init(strings.NewReader(phrase)) scnr.Error = func(*scanner.Scanner, string) { // scanner has the ability to invoke a callback upon tokenization errors. By default, if no handler is provided // then errors are printed to stdout. This handler is provided to suppress this output. // Suppressing these errors is not a problem in this case since the scanExpression function should see all tokens // and accumulate them as part of a version value if it is not a token of interest. The text/scanner splits on // a pre-configured set of "common" tokens (which we cannot provide). We are only interested in a sub-set of // these tokens, thus allow for input that would seemingly be invalid for this common set of tokens. // For example, the scanner finding `3.e` would interpret this as a float with no valid exponent. However, // this function accumulates all tokens into the version component (and versions are not guaranteed to have // valid tokens). } tokenRune := scnr.Scan() for tokenRune != scanner.EOF { currentToken := scnr.TokenText() switch { case currentToken == ",": captureVersionOperatorPair() case currentToken == "|" && lastToken == "|": captureVersionOperatorPair() captureAndGroup() case currentToken == "(" || currentToken == ")": return nil, fmt.Errorf("parenthetical expressions are not supported yet") case currentToken != "|": buf.Write([]byte(currentToken)) } lastToken = currentToken tokenRune = scnr.Scan() } captureVersionOperatorPair() captureAndGroup() return orGroups, nil } ================================================ FILE: grype/version/range_expression_test.go ================================================ package version import ( "testing" "github.com/go-test/deep" "github.com/stretchr/testify/require" ) func TestScanExpression(t *testing.T) { tests := []struct { name string phrase string expected [][]string wantErr require.ErrorAssertionFunc }{ { name: "simple AND and OR expression", phrase: "x,y||z", expected: [][]string{ { "x", "y", }, { "z", }, }, }, { name: "complex version constraints with operators", phrase: "<1.0, >=2.0|| 3.0 || =4.0", expected: [][]string{ { "<1.0", ">=2.0", }, { "3.0", }, { "=4.0", }, }, }, { name: "parenthetical expression not supported", phrase: "(<1.0, >=2.0|| 3.0) || =4.0", wantErr: require.Error, }, { name: "whitespace handling", phrase: ` > 1.0, <= 2.0,,, || = 3.0 `, expected: [][]string{ { ">1.0", "<=2.0", }, { "=3.0", }, }, }, { name: "quoted version with special characters", phrase: ` > 1.0, <= " (2.0||),,, ", || = 3.0 `, expected: [][]string{ { ">1.0", `<=" (2.0||),,, "`, }, { "=3.0", }, }, }, { name: "empty string", phrase: "", expected: nil, }, { name: "single version", phrase: "1.0", expected: [][]string{ { "1.0", }, }, }, { name: "only AND operators", phrase: ">=1.0, <2.0, !=1.5", expected: [][]string{ { ">=1.0", "<2.0", "!=1.5", }, }, }, { name: "only OR operators", phrase: "1.0 || 2.0 || 3.0", expected: [][]string{ { "1.0", }, { "2.0", }, { "3.0", }, }, }, { name: "single pipe character should be treated as version", phrase: "1.0|2.0", expected: [][]string{ { "1.02.0", }, }, }, { name: "multiple consecutive commas", phrase: "1.0,,,2.0", expected: [][]string{ { "1.0", "2.0", }, }, }, { name: "trailing comma", phrase: "1.0,2.0,", expected: [][]string{ { "1.0", "2.0", }, }, }, { name: "leading comma", phrase: ",1.0,2.0", expected: [][]string{ { "1.0", "2.0", }, }, }, { name: "complex version numbers", phrase: "1.0.0-alpha+build.1,2.0.0-beta.2||3.0.0-rc.1", expected: [][]string{ { "1.0.0-alpha+build.1", "2.0.0-beta.2", }, { "3.0.0-rc.1", }, }, }, { name: "parentheses at start", phrase: "(1.0", wantErr: require.Error, }, { name: "parentheses at end", phrase: "1.0)", wantErr: require.Error, }, { name: "special characters in version", phrase: "1.0.0+build.123-abc_def", expected: [][]string{ { "1.0.0+build.123-abc_def", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } actual, err := scanExpression(tt.phrase) tt.wantErr(t, err) if err != nil { return } for _, d := range deep.Equal(tt.expected, actual) { t.Errorf("difference: %+v", d) } }) } } ================================================ FILE: grype/version/range_test.go ================================================ package version import ( "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseRangeUnit(t *testing.T) { tests := []struct { phrase string expected *rangeUnit wantError require.ErrorAssertionFunc }{ { phrase: "", }, { phrase: `="in<(b e t w e e n)>quotes<=||>=not!="`, expected: &rangeUnit{ Operator: EQ, Version: "in<(b e t w e e n)>quotes<=||>=not!=", }, }, { phrase: ` >= "in<(b e t w e e n)>quotes<=||>=not!=" `, expected: &rangeUnit{ Operator: GTE, Version: "in<(b e t w e e n)>quotes<=||>=not!=", }, }, { // to cover a version that has quotes within it, but not necessarily surrounding the entire version phrase: ` >= inbet"ween)>quotes" with trailing words `, expected: &rangeUnit{ Operator: GTE, Version: `inbet"ween)>quotes" with trailing words`, }, }, { phrase: `="unbalandedquotes`, wantError: require.Error, }, { phrase: `="something"`, expected: &rangeUnit{ Operator: EQ, Version: "something", }, }, { phrase: "=something", expected: &rangeUnit{ Operator: EQ, Version: "something", }, }, { phrase: "= something", expected: &rangeUnit{ Operator: EQ, Version: "something", }, }, { phrase: "something", expected: &rangeUnit{ Operator: EQ, Version: "something", }, }, { phrase: "> something", expected: &rangeUnit{ Operator: GT, Version: "something", }, }, { phrase: ">= 2.3", expected: &rangeUnit{ Operator: GTE, Version: "2.3", }, }, { phrase: "< 2.3", expected: &rangeUnit{ Operator: LT, Version: "2.3", }, }, { phrase: "<=2.3", expected: &rangeUnit{ Operator: LTE, Version: "2.3", }, }, { phrase: " >= 1.0 ", expected: &rangeUnit{ Operator: GTE, Version: "1.0", }, }, } for _, test := range tests { t.Run(test.phrase, func(t *testing.T) { if test.wantError == nil { test.wantError = require.NoError } actual, err := parseRange(test.phrase) test.wantError(t, err) if err != nil { return } if !reflect.DeepEqual(test.expected, actual) { t.Errorf("expected: '%+v' got: '%+v'", test.expected, actual) } }) } } func TestTrimQuotes(t *testing.T) { tests := []struct { name string input string expected string err bool }{ { name: "no quotes", input: "test", expected: "test", err: true, }, { name: "double quotes", input: "\"test\"", expected: "test", err: false, }, { name: "single quotes", input: "'test'", expected: "test", err: false, }, { name: "leading_single_quote", input: "'test", expected: "'test", err: true, }, { name: "trailing_single_quote", input: "test'", expected: "test'", err: true, }, { name: "leading_double_quote", input: "'test", expected: "'test", err: true, }, { name: "trailing_double_quote", input: "test'", expected: "test'", err: true, }, { // this raises an error, but I do not believe that this is a scenario that we need to account for, so should be ok. name: "nested double/double quotes", input: "\"t\"es\"t\"", expected: "\"t\"es\"t\"", err: true, }, { name: "nested single/single quotes", input: "'t'es't'", expected: "t'es't", err: false, }, { name: "nested single/double quotes", input: "'t\"es\"t'", expected: "t\"es\"t", err: false, }, { name: "nested double/single quotes", input: "\"t'es't\"", expected: "t'es't", err: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := trimQuotes(test.input) if test.err { assert.NotNil(t, err, "expected an error but did not get one") } else { assert.Nil(t, err, "expected no error, got \"%+v\"", err) } assert.Equal(t, actual, test.expected, "output does not match expected: exp:%v got:%v", test.expected, actual) }) } } ================================================ FILE: grype/version/rpm_version.go ================================================ package version import ( "fmt" "reflect" "regexp" "strconv" "strings" "unicode" ) var _ Comparator = (*rpmVersion)(nil) type rpmVersion struct { epoch *int version string release string } func newRpmVersion(raw string) (rpmVersion, error) { epoch, remainingVersion, err := splitEpochFromVersion(raw) if err != nil { return rpmVersion{}, err } fields := strings.SplitN(remainingVersion, "-", 2) version := fields[0] var release string if len(fields) > 1 { // there is a release release = fields[1] } return rpmVersion{ epoch: epoch, version: version, release: release, }, nil } func (v rpmVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newRpmVersion(other.Raw) if err != nil { return 0, err } return v.compare(o), nil } // CompareWithConfig compares two RPM versions using the provided comparison // configuration. The config controls behavior for missing epochs: // - "zero" strategy: missing epochs are treated as 0 // - "auto" strategy: missing epochs in the package version match the constraint's epoch // // Returns: // // -1 if v < other // 0 if v == other // 1 if v > other // // Only the package version's (v) missing epoch is handled by the auto strategy. If the // constraint (other) is missing an epoch, it is always treated as 0 per RPM specification. func (v rpmVersion) CompareWithConfig(other *Version, cfg ComparisonConfig) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newRpmVersion(other.Raw) if err != nil { return 0, err } return v.compareWithConfig(o, cfg), nil } func (v rpmVersion) compareWithConfig(v2 rpmVersion, cfg ComparisonConfig) int { if reflect.DeepEqual(v, v2) { return 0 } // Handle epoch comparison based on strategy switch cfg.MissingEpochStrategy { case MissingEpochStrategyAuto: // If v (package) is missing epoch but v2 (constraint) has one, temporarily use v2's epoch for v if !epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) { vWithEpoch := v vWithEpoch.epoch = v2.epoch return vWithEpoch.compare(v2) } case MissingEpochStrategyZero: // If v (package) is missing epoch, treat it as 0 // This differs from the default compare() behavior which ignores one-sided epochs if !epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) { vWithEpoch := v zero := 0 vWithEpoch.epoch = &zero return vWithEpoch.compare(v2) } } return v.compare(v2) } // Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2. // This a pragmatic adaptation of comparison for the messy data // encountered in vuln scanning. If epochs are NOT present and explicit // (e.g. >= 0) in both versions then they are ignored for the comparison. // For a rpm spec-compliant comparison, see strictCompare() instead func (v rpmVersion) compare(v2 rpmVersion) int { if reflect.DeepEqual(v, v2) { return 0 } // Only compare epochs if both are present and explicit. This is technically // against what RedHat says to do with missing epoch (which is to assume a 0 epoch). // However, since we may be dealing with upstream data sources where there is an epoch // for a package but the value was stripped, the best we can do is to compare only the // version values without the epoch values. if epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) { epochResult := compareEpochs(*v.epoch, *v2.epoch) if epochResult != 0 { return epochResult } } ret := compareRpmVersions(v.version, v2.version) if ret != 0 { return ret } return compareRpmVersions(v.release, v2.release) } func epochIsPresent(epoch *int) bool { return epoch != nil } // Epoch comparison, standard int comparison for sorting func compareEpochs(e1 int, e2 int) int { switch { case e1 > e2: return 1 case e1 < e2: return -1 default: return 0 } } func (v rpmVersion) String() string { version := "" if v.epoch != nil { version += fmt.Sprintf("%d:", *v.epoch) } version += v.version if v.release != "" { version += fmt.Sprintf("-%s", v.release) } return version } func splitEpochFromVersion(rawVersion string) (*int, string, error) { fields := strings.SplitN(rawVersion, ":", 2) // When the epoch is not included, should be considered to be 0 during // comparisons (see https://github.com/rpm-software-management/rpm/issues/450). // But, often the inclusion of the epoch in vuln databases or source RPM // filenames is not consistent so, represent a missing epoch as nil. This allows // the comparison logic itself to determine if it should use a zero or another // value which supports more flexible comparison options because the version // creation is not lossy if len(fields) == 1 { return nil, rawVersion, nil } // there is an epoch epochStr := strings.TrimLeft(fields[0], " ") epoch, err := strconv.Atoi(epochStr) if err != nil { return nil, "", fmt.Errorf("unable to parse epoch (%s): %w", epochStr, err) } return &epoch, fields[1], nil } // compareRpmVersions compares two version or release strings without the epoch. // Source: https://github.com/cavaliercoder/go-rpm/blob/master/version.go // // For the original C implementation, see: // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16 var alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)") //nolint:funlen,gocognit,dupl // see comparePacmanVersions for why we keep these decoupled func compareRpmVersions(a, b string) int { // shortcut for equality if a == b { return 0 } // get alpha/numeric segments segsa := alphanumPattern.FindAllString(a, -1) segsb := alphanumPattern.FindAllString(b, -1) maxSegs := max(len(segsa), len(segsb)) minSegs := min(len(segsa), len(segsb)) // compare each segment for i := 0; i < minSegs; i++ { a := segsa[i] b := segsb[i] // compare tildes if []rune(a)[0] == '~' || []rune(b)[0] == '~' { if []rune(a)[0] != '~' { return 1 } if []rune(b)[0] != '~' { return -1 } } if unicode.IsNumber([]rune(a)[0]) { // numbers are always greater than alphas if !unicode.IsNumber([]rune(b)[0]) { // a is numeric, b is alpha return 1 } // trim leading zeros a = strings.TrimLeft(a, "0") b = strings.TrimLeft(b, "0") // longest string wins without further comparison if len(a) > len(b) { return 1 } else if len(b) > len(a) { return -1 } } else if unicode.IsNumber([]rune(b)[0]) { // a is alpha, b is numeric return -1 } // string compare if a < b { return -1 } else if a > b { return 1 } } // segments were all the same but separators must have been different if len(segsa) == len(segsb) { return 0 } // If there is a tilde in a segment past the min number of segments, find it. if len(segsa) > minSegs && []rune(segsa[minSegs])[0] == '~' { return -1 } else if len(segsb) > minSegs && []rune(segsb[minSegs])[0] == '~' { return 1 } // are the remaining segments 0s? segaAll0s := true segbAll0s := true for i := minSegs; i < maxSegs; i++ { if i < len(segsa) && segsa[i] != "0" { segaAll0s = false } if i < len(segsb) && segsb[i] != "0" { segbAll0s = false } } if segaAll0s && segbAll0s { return 0 } // whoever has the most segments wins if len(segsa) > len(segsb) { return 1 } return -1 } ================================================ FILE: grype/version/rpm_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRpmVersion_Constraint(t *testing.T) { tests := []testCase{ // empty values {version: "2.3.1", constraint: "", satisfied: true}, // trivial compound conditions {version: "2.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.3.1", constraint: "> 1.0.0, < 2.0.0", satisfied: true}, {version: "2.0.0", constraint: "> 1.0.0, <= 2.0.0", satisfied: true}, {version: "2.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.0.0", constraint: ">= 1.0.0, < 2.0.0", satisfied: true}, {version: "1.0.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "0.9.0", constraint: "> 1.0.0, < 2.0.0", satisfied: false}, {version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, // trivial scenarios {version: "2.3.1", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "< 2.0", satisfied: false}, {version: "2.3.1", constraint: "< 2", satisfied: false}, {version: "2.3.1", constraint: "< 2.3", satisfied: false}, {version: "2.3.1", constraint: "< 2.3.1", satisfied: false}, {version: "2.3.1", constraint: "< 2.3.2", satisfied: true}, {version: "2.3.1", constraint: "< 2.4", satisfied: true}, {version: "2.3.1", constraint: "< 3", satisfied: true}, {version: "2.3.1", constraint: "< 3.0", satisfied: true}, {version: "2.3.1", constraint: "< 3.0.0", satisfied: true}, // epoch {version: "1:0", constraint: "< 0:1", satisfied: false}, {version: "2:4.19.01-1.el7_5", constraint: "< 2:4.19.1-1.el7_5", satisfied: false}, {version: "2:4.19.01-1.el7_5", constraint: "<= 2:4.19.1-1.el7_5", satisfied: true}, {version: "0:4.19.1-1.el7_5", constraint: "< 2:4.19.1-1.el7_5", satisfied: true}, {version: "11:4.19.0-1.el7_5", constraint: "< 12:4.19.0-1.el7", satisfied: true}, {version: "13:4.19.0-1.el7_5", constraint: "< 12:4.19.0-1.el7", satisfied: false}, // regression: https://github.com/anchore/grype/issues/316 {version: "1.5.4-2.el7_9", constraint: "< 0:1.5.4-2.el7_9", satisfied: false}, {version: "1.5.4-2.el7", constraint: "< 0:1.5.4-2.el7_9", satisfied: true}, // Non-standard epoch handling. In comparisons with epoch on only one side, they are both ignored {version: "1:0", constraint: "< 1", satisfied: true}, {version: "0:0", constraint: "< 0", satisfied: false}, {version: "0:0", constraint: "= 0", satisfied: true}, {version: "0", constraint: "= 0:0", satisfied: true}, {version: "1.0", constraint: "< 2:1.0", satisfied: false}, {version: "1.0", constraint: "<= 2:1.0", satisfied: true}, {version: "1:2", constraint: "< 1", satisfied: false}, {version: "1:2", constraint: "> 1", satisfied: true}, {version: "2:4.19.01-1.el7_5", constraint: "< 4.19.1-1.el7_5", satisfied: false}, {version: "2:4.19.01-1.el7_5", constraint: "<= 4.19.1-1.el7_5", satisfied: true}, {version: "4.19.01-1.el7_5", constraint: "< 2:4.19.1-1.el7_5", satisfied: false}, {version: "4.19.0-1.el7_5", constraint: "< 12:4.19.0-1.el7", satisfied: false}, {version: "4.19.0-1.el7_5", constraint: "<= 12:4.19.0-1.el7", satisfied: false}, {version: "3:4.19.0-1.el7_5", constraint: "< 4.21.0-1.el7", satisfied: true}, {version: "4:1.2.3-3-el7_5", constraint: "< 1.2.3-el7_5~snapshot1", satisfied: false}, // regression https://github.com/anchore/grype/issues/398 {version: "8.3.1-5.el8.4", constraint: "< 0:8.3.1-5.el8.5", satisfied: true}, {version: "8.3.1-5.el8.40", constraint: "< 0:8.3.1-5.el8.5", satisfied: false}, {version: "8.3.1-5.el8", constraint: "< 0:8.3.1-5.el8.0.0", satisfied: false}, {version: "8.3.1-5.el8", constraint: "<= 0:8.3.1-5.el8.0.0", satisfied: true}, {version: "8.3.1-5.el8.0.0", constraint: "> 0:8.3.1-5.el8", satisfied: false}, {version: "8.3.1-5.el8.0.0", constraint: ">= 0:8.3.1-5.el8", satisfied: true}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, RpmFormat) assert.NoError(t, err, "unexpected error from newRpmConstraint: %v", err) test.assertVersionConstraint(t, RpmFormat, constraint) }) } } func TestRpmVersion_Compare(t *testing.T) { tests := []struct { v1 string v2 string result int }{ // from https://github.com/anchore/anchore-engine/blob/a447ee951c2d4e17c2672553d7280cfdb5e5f193/tests/unit/anchore_engine/util/test_rpm.py {"1", "1", 0}, {"4.19.0a-1.el7_5", "4.19.0c-1.el7", -1}, {"4.19.0-1.el7_5", "4.21.0-1.el7", -1}, {"4.19.01-1.el7_5", "4.19.10-1.el7_5", -1}, {"4.19.0-1.el7_5", "4.19.0-1.el7", 1}, {"4.19.0-1.el7_5", "4.17.0-1.el7", 1}, {"4.19.01-1.el7_5", "4.19.1-1.el7_5", 0}, {"4.19.1-1.el7_5", "4.19.1-01.el7_5", 0}, {"4.19.1", "4.19.1", 0}, {"1.2.3-el7_5~snapshot1", "1.2.3-3-el7_5", -1}, {"1:0", "0:1", 1}, {"1:2", "1", 1}, {"0:4.19.1-1.el7_5", "2:4.19.1-1.el7_5", -1}, {"4:1.2.3-3-el7_5", "1.2.3-el7_5~snapshot1", 1}, // non-standard comparisons that ignore epochs due to only one being available {"1:0", "1", -1}, {"2:4.19.01-1.el7_5", "4.19.1-1.el7_5", 0}, {"4.19.01-1.el7_5", "2:4.19.1-1.el7_5", 0}, {"4.19.0-1.el7_5", "12:4.19.0-1.el7", 1}, {"3:4.19.0-1.el7_5", "4.21.0-1.el7", -1}, } for _, test := range tests { name := test.v1 + "_vs_" + test.v2 t.Run(name, func(t *testing.T) { v1 := New(test.v1, RpmFormat) v2 := New(test.v2, RpmFormat) actual, err := v1.Compare(v2) require.NoError(t, err, "unexpected error comparing versions: %s vs %s", test.v1, test.v2) assert.Equal(t, test.result, actual, "expected comparison result to match for %s vs %s", test.v1, test.v2) }) } } func TestRpmVersion_Compare_Format(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3-1", otherVersion: "1.2.3-2", otherFormat: RpmFormat, expectError: false, }, { name: "same format successful comparison with epoch", thisVersion: "1:1.2.3-1", otherVersion: "1:1.2.3-2", otherFormat: RpmFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid rpm format", thisVersion: "1.2.3-1", otherVersion: "1.2.3-2", otherFormat: UnknownFormat, expectError: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer := New(test.thisVersion, RpmFormat) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestRpmVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3-1", RpmFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } func TestRpmVersion_CompareWithConfig(t *testing.T) { tests := []struct { name string version string other string strategy MissingEpochStrategy want int // -1, 0, or 1 }{ { name: "package has epoch, no behavior change with auto", version: "1:2.0.0", other: "1:1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // 1:2.0.0 > 1:1.5.0 }, { name: "package has epoch, no behavior change with zero", version: "1:2.0.0", other: "1:1.5.0", strategy: "zero", want: 1, // 1:2.0.0 > 1:1.5.0 }, { name: "package missing epoch, constraint has epoch, auto strategy - no match", version: "2.0.0", other: "1:1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // Treated as 1:2.0.0 > 1:1.5.0 }, { name: "package missing epoch, constraint has epoch, zero strategy", version: "2.0.0", other: "1:1.5.0", strategy: "zero", want: -1, // Treated as 0:2.0.0 < 1:1.5.0 (epoch 0 < 1) }, { name: "both missing epoch, auto strategy", version: "2.0.0", other: "1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // 2.0.0 > 1.5.0 }, { name: "both missing epoch, zero strategy", version: "2.0.0", other: "1.5.0", strategy: "zero", want: 1, // 2.0.0 > 1.5.0 }, { name: "constraint missing epoch, package has epoch", version: "1:2.0.0", other: "1.5.0", strategy: MissingEpochStrategyAuto, want: 1, // 1:2.0.0 > 0:1.5.0 (constraint gets epoch 0) }, { name: "auto strategy, package less than constraint", version: "1.0.0", other: "1:1.5.0", strategy: MissingEpochStrategyAuto, want: -1, // Treated as 1:1.0.0 < 1:1.5.0 }, { name: "auto strategy, different epochs on constraints", version: "1.2.0", other: "2:1.5.0", strategy: MissingEpochStrategyAuto, want: -1, // Treated as 2:1.2.0 < 2:1.5.0 }, { name: "zero strategy, package version newer but lower epoch", version: "3.0.0", other: "1:1.0.0", strategy: "zero", want: -1, // Treated as 0:3.0.0 < 1:1.0.0 (epoch 0 < 1) }, { name: "auto strategy, equal versions different missing epochs", version: "1.2.3", other: "1:1.2.3", strategy: MissingEpochStrategyAuto, want: 0, // Treated as 1:1.2.3 == 1:1.2.3 }, { name: "zero strategy, equal versions different missing epochs", version: "1.2.3", other: "1:1.2.3", strategy: "zero", want: -1, // Treated as 0:1.2.3 < 1:1.2.3 (epoch 0 < 1) }, { name: "auto strategy, large epoch difference", version: "1.0.0", other: "999:0.5.0", strategy: MissingEpochStrategyAuto, want: 1, // Treated as 999:1.0.0 > 999:0.5.0 }, { name: "zero strategy, large epoch difference", version: "1.0.0", other: "999:0.5.0", strategy: "zero", want: -1, // Treated as 0:1.0.0 < 999:0.5.0 (epoch 0 < 999) }, { name: "both have epochs, strategy should not matter", version: "2:1.5.0", other: "1:2.0.0", strategy: MissingEpochStrategyAuto, want: 1, // 2:1.5.0 > 1:2.0.0 (epoch takes precedence) }, { name: "both have same epoch, strategy should not matter", version: "3:2.0.0", other: "3:1.5.0", strategy: "zero", want: 1, // 3:2.0.0 > 3:1.5.0 }, { name: "empty strategy uses default compare behavior (ignores one-sided epochs)", version: "2.0.0", other: "1:1.5.0", strategy: "", want: 1, // Falls through to compare() which ignores one-sided epochs: 2.0.0 > 1.5.0 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v1, err := newRpmVersion(tt.version) require.NoError(t, err) v2 := &Version{ Format: RpmFormat, Raw: tt.other, } cfg := ComparisonConfig{ MissingEpochStrategy: tt.strategy, } result, err := v1.CompareWithConfig(v2, cfg) require.NoError(t, err) assert.Equal(t, tt.want, result, "comparing %s vs %s with strategy %s", tt.version, tt.other, tt.strategy) }) } } func TestRpmVersion_CompareWithConfig_ErrorCases(t *testing.T) { tests := []struct { name string version string other *Version strategy MissingEpochStrategy wantErr bool }{ { name: "nil other version", version: "1.0.0", other: nil, strategy: MissingEpochStrategyAuto, wantErr: true, }, { name: "invalid other version format", version: "1.0.0", other: &Version{Format: RpmFormat, Raw: "not:a:valid:version:string:with:too:many:colons"}, strategy: MissingEpochStrategyAuto, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v1, err := newRpmVersion(tt.version) require.NoError(t, err) cfg := ComparisonConfig{ MissingEpochStrategy: tt.strategy, } _, err = v1.CompareWithConfig(tt.other, cfg) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestRpmVersion_CompareWithConfig_ConsistencyWithCompare(t *testing.T) { // Test that when both versions have epochs, CompareWithConfig gives same result as Compare tests := []struct { v1 string v2 string }{ {"1:2.0.0", "1:1.5.0"}, {"2:1.0.0", "1:2.0.0"}, {"0:1.2.3", "0:1.2.3"}, {"5:1.0.0-1.el7", "5:1.0.0-2.el7"}, } for _, tt := range tests { t.Run(tt.v1+"_vs_"+tt.v2, func(t *testing.T) { v1, _ := newRpmVersion(tt.v1) v2 := &Version{Format: RpmFormat, Raw: tt.v2} // Test with both strategies for _, strategy := range []MissingEpochStrategy{MissingEpochStrategyZero, MissingEpochStrategyAuto} { cfg := ComparisonConfig{MissingEpochStrategy: strategy} resultWithConfig, err1 := v1.CompareWithConfig(v2, cfg) require.NoError(t, err1) resultNormal, err2 := v1.Compare(v2) require.NoError(t, err2) assert.Equal(t, resultNormal, resultWithConfig, "when both versions have epochs, CompareWithConfig should match Compare (strategy: %s)", strategy) } }) } } ================================================ FILE: grype/version/semantic_version.go ================================================ package version import ( "regexp" "strings" hashiVer "github.com/anchore/go-version" ) var _ Comparator = (*semanticVersion)(nil) // semverPrereleaseNormalizer are meant to replace common pre-release suffixes with standard semver pre-release suffixes. // this is primarily intended for to cover ruby packages such as activerecord and sprockets, which don't strictly // follow semver, however, this can generally be applied to other cases using semver as well. // note: this may result in missed matches for versioned betas var semverPrereleaseNormalizer = strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc") type semanticVersion struct { obj *hashiVer.Version } var versionStartsWithV = regexp.MustCompile(`^v\d+`) func newSemanticVersion(raw string, strict bool) (semanticVersion, error) { clean := semverPrereleaseNormalizer.Replace(raw) var verObj *hashiVer.Version var err error if strict { // we still want v-prefix processing if versionStartsWithV.MatchString(clean) { clean = strings.TrimPrefix(clean, "v") } verObj, err = hashiVer.NewSemver(clean) } else { verObj, err = hashiVer.NewVersion(clean) } if err != nil { return semanticVersion{}, invalidFormatError(SemanticFormat, raw, err) } return semanticVersion{ obj: verObj, }, nil } func (v semanticVersion) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } o, err := newSemanticVersion(other.Raw, false) if err != nil { return 0, err } return v.obj.Compare(o.obj), nil } ================================================ FILE: grype/version/semantic_version_test.go ================================================ package version import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSemanticVersion_Constraint(t *testing.T) { tests := []testCase{ // empty values {version: "2.3.1", constraint: "", satisfied: true}, // typical cases {version: "0.9.9-r0", constraint: "< 0.9.12-r1", satisfied: true}, // regression case {version: "1.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.2.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: true}, {version: "0.0.1", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "0.6.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.5.0", constraint: "> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "2.3.1", satisfied: true}, {version: "2.3.1", constraint: "= 2.3.1", satisfied: true}, {version: "2.3.1", constraint: " = 2.3.1", satisfied: true}, {version: "2.3.1", constraint: ">= 2.3.1", satisfied: true}, {version: "2.3.1", constraint: "> 2.0.0", satisfied: true}, {version: "2.3.1", constraint: "> 2.0", satisfied: true}, {version: "2.3.1", constraint: "> 2", satisfied: true}, {version: "2.3.1", constraint: "> 2, < 3", satisfied: true}, {version: "2.3.1", constraint: "> 2.3, < 3.1", satisfied: true}, {version: "2.3.1", constraint: "> 2.3.0, < 3.1", satisfied: true}, {version: "2.3.1", constraint: ">= 2.3.1, < 3.1", satisfied: true}, {version: "2.3.1", constraint: " = 2.3.2", satisfied: false}, {version: "2.3.1", constraint: ">= 2.3.2", satisfied: false}, {version: "2.3.1", constraint: "> 2.3.1", satisfied: false}, {version: "2.3.1", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1", constraint: "< 2.0", satisfied: false}, {version: "2.3.1", constraint: "< 2", satisfied: false}, {version: "2.3.1", constraint: "< 2, > 3", satisfied: false}, {version: "2.3.1+meta", constraint: "2.3.1", satisfied: true}, {version: "2.3.1+meta", constraint: "= 2.3.1", satisfied: true}, {version: "2.3.1+meta", constraint: " = 2.3.1", satisfied: true}, {version: "2.3.1+meta", constraint: ">= 2.3.1", satisfied: true}, {version: "2.3.1+meta", constraint: "> 2.0.0", satisfied: true}, {version: "2.3.1+meta", constraint: "> 2.0", satisfied: true}, {version: "2.3.1+meta", constraint: "> 2", satisfied: true}, {version: "2.3.1+meta", constraint: "> 2, < 3", satisfied: true}, {version: "2.3.1+meta", constraint: "> 2.3, < 3.1", satisfied: true}, {version: "2.3.1+meta", constraint: "> 2.3.0, < 3.1", satisfied: true}, {version: "2.3.1+meta", constraint: ">= 2.3.1, < 3.1", satisfied: true}, {version: "2.3.1+meta", constraint: " = 2.3.2", satisfied: false}, {version: "2.3.1+meta", constraint: ">= 2.3.2", satisfied: false}, {version: "2.3.1+meta", constraint: "> 2.3.1", satisfied: false}, {version: "2.3.1+meta", constraint: "< 2.0.0", satisfied: false}, {version: "2.3.1+meta", constraint: "< 2.0", satisfied: false}, {version: "2.3.1+meta", constraint: "< 2", satisfied: false}, {version: "2.3.1+meta", constraint: "< 2, > 3", satisfied: false}, // from https://github.com/hashicorp/go-version/issues/61 // and https://semver.org/#spec-item-11 // A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. {version: "1.0.0-alpha", constraint: "> 1.0.0-alpha.1", satisfied: false}, {version: "1.0.0-alpha", constraint: "< 1.0.0-alpha.1", satisfied: true}, {version: "1.0.0-alpha.1", constraint: "> 1.0.0-alpha.beta", satisfied: false}, {version: "1.0.0-alpha.1", constraint: "< 1.0.0-alpha.beta", satisfied: true}, {version: "1.0.0-alpha.beta", constraint: "> 1.0.0-beta", satisfied: false}, {version: "1.0.0-alpha.beta", constraint: "< 1.0.0-beta", satisfied: true}, {version: "1.0.0-beta", constraint: "> 1.0.0-beta.2", satisfied: false}, {version: "1.0.0-beta", constraint: "< 1.0.0-beta.2", satisfied: true}, {version: "1.0.0-beta.2", constraint: "> 1.0.0-beta.11", satisfied: false}, {version: "1.0.0-beta.2", constraint: "< 1.0.0-beta.11", satisfied: true}, {version: "1.0.0-beta.11", constraint: "> 1.0.0-rc.1", satisfied: false}, {version: "1.0.0-beta.11", constraint: "< 1.0.0-rc.1", satisfied: true}, {version: "1.0.0-rc.1", constraint: "> 1.0.0", satisfied: false}, {version: "1.0.0-rc.1", constraint: "< 1.0.0", satisfied: true}, {version: "1.20rc1", constraint: " = 1.20.0-rc1", satisfied: true}, {version: "1.21rc2", constraint: " = 1.21.1", satisfied: false}, {version: "1.21rc2", constraint: " = 1.21", satisfied: false}, {version: "1.21rc2", constraint: " = 1.21-rc2", satisfied: true}, {version: "1.21rc2", constraint: " = 1.21.0-rc2", satisfied: true}, {version: "1.21rc2", constraint: " = 1.21.0rc2", satisfied: true}, {version: "1.0.0-alpha.1", constraint: "> 1.0.0-alpha.1", satisfied: false}, {version: "1.0.0-alpha.2", constraint: "> 1.0.0-alpha.1", satisfied: true}, {version: "1.2.0-beta", constraint: ">1.0, <2.0", satisfied: true}, {version: "1.2.0-beta", constraint: ">1.0", satisfied: true}, {version: "1.2.0-beta", constraint: "<2.0", satisfied: true}, {version: "1.2.0", constraint: ">1.0, <2.0", satisfied: true}, // below are test cases for the ruby version normalizer that converts .alpha, .beta, .rc to -alpha, -beta, -rc // prerelease normalizer - alpha versions {version: "1.0.0.alpha", constraint: "< 1.0.0", satisfied: true}, {version: "1.0.0.alpha", constraint: "> 1.0.0-alpha", satisfied: false}, // should be equal after normalization {version: "1.0.0.alpha", constraint: "= 1.0.0-alpha", satisfied: true}, {version: "1.0.0.alpha1", constraint: "= 1.0.0-alpha1", satisfied: true}, {version: "1.0.0.alpha.1", constraint: "= 1.0.0-alpha.1", satisfied: true}, // prerelease normalizer - beta versions {version: "1.0.0.beta", constraint: "< 1.0.0", satisfied: true}, {version: "1.0.0.beta", constraint: "> 1.0.0-alpha", satisfied: true}, {version: "1.0.0.beta", constraint: "= 1.0.0-beta", satisfied: true}, {version: "1.0.0.beta2", constraint: "= 1.0.0-beta2", satisfied: true}, {version: "1.0.0.beta.2", constraint: "= 1.0.0-beta.2", satisfied: true}, // prerelease normalizer - rc versions {version: "1.0.0.rc", constraint: "< 1.0.0", satisfied: true}, {version: "1.0.0.rc", constraint: "> 1.0.0-beta", satisfied: true}, {version: "1.0.0.rc", constraint: "= 1.0.0-rc", satisfied: true}, {version: "1.0.0.rc1", constraint: "= 1.0.0-rc1", satisfied: true}, {version: "1.0.0.rc.1", constraint: "= 1.0.0-rc.1", satisfied: true}, // prerelease normalizer - ordering tests to ensure normalization doesn't break semver precedence {version: "1.0.0.alpha", constraint: "< 1.0.0-beta", satisfied: true}, {version: "1.0.0.beta", constraint: "< 1.0.0-rc", satisfied: true}, {version: "1.0.0.rc", constraint: "< 1.0.0", satisfied: true}, {version: "1.0.0.alpha1", constraint: "< 1.0.0-alpha2", satisfied: true}, // prerelease normalizer - mixed ruby and standard semver styles in constraints {version: "1.0.0.alpha", constraint: "< 1.0.0-beta", satisfied: true}, {version: "1.0.0-alpha", constraint: "< 1.0.0-beta", satisfied: true}, // prerelease normalizer - complex constraints with ruby-style versions {version: "1.0.0.alpha", constraint: "> 0.9.0, < 1.0.0", satisfied: true}, {version: "1.0.0.beta", constraint: "> 1.0.0-alpha, < 1.0.0", satisfied: true}, {version: "2.1.0.rc1", constraint: "> 2.0.0, < 2.1.0", satisfied: true}, // prerelease normalizer - edge cases {version: "1.0.0.alpha.beta", constraint: "= 1.0.0-alpha-beta", satisfied: true}, // multiple replacements {version: "1.0.0.rc.alpha", constraint: "= 1.0.0-rc-alpha", satisfied: true}, // mixed order // prerelease normalizer - ensure regular versions still work {version: "1.0.0-alpha", constraint: "< 1.0.0", satisfied: true}, {version: "1.0.0-beta", constraint: "> 1.0.0-alpha", satisfied: true}, {version: "1.0.0-rc", constraint: "> 1.0.0-beta", satisfied: true}, } for _, test := range tests { t.Run(test.tName(), func(t *testing.T) { constraint, err := GetConstraint(test.constraint, SemanticFormat) assert.NoError(t, err) test.assertVersionConstraint(t, SemanticFormat, constraint) }) } } func TestSemanticVersion_PrereleaseNormalizer_EdgeCases(t *testing.T) { // test edge cases to ensure the normalizer can be safely retained tests := []struct { name string version string wantError require.ErrorAssertionFunc }{ { name: "version with only alpha", version: "alpha", wantError: require.Error, // invalid semver }, { name: "version with leading alpha", version: "alpha.1.0.0", wantError: require.Error, // invalid semver }, { name: "empty version", version: "", wantError: require.Error, }, { name: "version with multiple dots in prerelease", version: "1.0.0.alpha.beta.rc", // should normalize to 1.0.0-alpha-beta-rc }, { name: "version already in correct format", version: "1.0.0-alpha", }, { name: "version with build metadata", version: "1.0.0.alpha+build", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.wantError == nil { test.wantError = require.NoError } _, err := newSemanticVersion(test.version, false) test.wantError(t, err, "expected error for version: %s", test.version) }) } } func TestSemanticVersion_PrereleaseNormalizer_WithGemFormat(t *testing.T) { // ensure that the prerelease normalizer in semantic format doesn't conflict with gem format rubyStyleVersions := []string{ "1.0.0.alpha", "1.0.0.beta.1", "1.0.0.rc2", } for _, version := range rubyStyleVersions { t.Run(version, func(t *testing.T) { // both semantic and gem formats should be able to handle these versions semanticVer := New(version, SemanticFormat) gemVer := New(version, GemFormat) // they might have different comparison behavior, but both should be valid assert.NotNil(t, semanticVer) assert.NotNil(t, gemVer) }) } } func TestSemanticVersion_Compare_Format(t *testing.T) { tests := []struct { name string thisVersion string otherVersion string otherFormat Format expectError bool errorSubstring string }{ { name: "same format successful comparison", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: SemanticFormat, expectError: false, }, { name: "same format successful comparison with prerelease", thisVersion: "1.2.3-alpha", otherVersion: "1.2.3-beta", otherFormat: SemanticFormat, expectError: false, }, { name: "same format successful comparison with build metadata", thisVersion: "1.2.3+build.1", otherVersion: "1.2.3+build.2", otherFormat: SemanticFormat, expectError: false, }, { name: "unknown format attempts upgrade - valid semantic format", thisVersion: "1.2.3", otherVersion: "1.2.4", otherFormat: UnknownFormat, expectError: false, }, { name: "unknown format attempts upgrade - invalid semantic format", thisVersion: "1.2.3", otherVersion: "not.valid.semver", otherFormat: UnknownFormat, expectError: true, errorSubstring: "invalid", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, err := newSemanticVersion(test.thisVersion, true) require.NoError(t, err) otherVer := New(test.otherVersion, test.otherFormat) result, err := thisVer.Compare(otherVer) if test.expectError { require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } } else { assert.NoError(t, err) assert.Contains(t, []int{-1, 0, 1}, result, "Expected comparison result to be -1, 0, or 1") } }) } } func TestSemanticVersion_Compare_EdgeCases(t *testing.T) { tests := []struct { name string setupFunc func(testing.TB) (*Version, *Version) expectError bool errorSubstring string }{ { name: "nil version object", setupFunc: func(t testing.TB) (*Version, *Version) { thisVer := New("1.2.3", SemanticFormat) return thisVer, nil }, expectError: true, errorSubstring: "no version provided for comparison", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { thisVer, otherVer := test.setupFunc(t) _, err := thisVer.Compare(otherVer) require.Error(t, err) if test.errorSubstring != "" { assert.True(t, strings.Contains(err.Error(), test.errorSubstring), "Expected error to contain '%s', got: %v", test.errorSubstring, err) } }) } } ================================================ FILE: grype/version/set.go ================================================ package version import ( "sort" ) type Set struct { versions map[string]*Version getKey func(v *Version) string } func NewSet(ignoreFormat bool, vs ...*Version) *Set { var getKey func(v *Version) string if ignoreFormat { getKey = func(v *Version) string { if v == nil { return "" } return v.Raw } } else { getKey = func(v *Version) string { if v == nil { return "" } return v.Raw + ":" + v.Format.String() } } s := &Set{ versions: make(map[string]*Version), getKey: getKey, } s.Add(vs...) return s } func (s *Set) Add(vs ...*Version) { if s.versions == nil { s.versions = make(map[string]*Version) } for _, v := range vs { if v == nil { continue } key := s.getKey(v) s.versions[key] = v } } func (s *Set) Remove(vs ...*Version) { if s.versions == nil { return } for _, v := range vs { if v == nil { continue } key := s.getKey(v) delete(s.versions, key) } } func (s *Set) Contains(v *Version) bool { if v == nil || s.versions == nil { return false } key := s.getKey(v) _, exists := s.versions[key] return exists } func (s *Set) Values() []*Version { if len(s.versions) == 0 { return nil } out := make([]*Version, 0, len(s.versions)) for _, v := range s.versions { out = append(out, v) } sort.Slice(out, func(i, j int) bool { if out[i] == nil && out[j] == nil { return false } if out[i] == nil { return true } if out[j] == nil { return false } cmp, err := out[i].Compare(out[j]) if err != nil { return false // if we can't compare, don't change the order } return cmp < 0 }) return out } func (s *Set) Size() int { if s.versions == nil { return 0 } return len(s.versions) } func (s *Set) Clear() { if s.versions != nil { s.versions = make(map[string]*Version) } } ================================================ FILE: grype/version/set_test.go ================================================ package version import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewSet(t *testing.T) { tests := []struct { name string ignoreFormat bool versions []*Version expectedSize int }{ { name: "empty set", ignoreFormat: false, versions: nil, expectedSize: 0, }, { name: "set with versions", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), New("2.0.0", SemanticFormat), }, expectedSize: 2, }, { name: "set with duplicate versions ignoring format", ignoreFormat: true, versions: []*Version{ New("1.0.0", SemanticFormat), New("1.0.0", ApkFormat), }, expectedSize: 1, }, { name: "set with duplicate versions not ignoring format", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), New("1.0.0", ApkFormat), }, expectedSize: 2, }, { name: "set with nil versions", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), nil, New("2.0.0", SemanticFormat), }, expectedSize: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.versions...) assert.Equal(t, tt.expectedSize, s.Size()) }) } } func TestSet_Add(t *testing.T) { tests := []struct { name string ignoreFormat bool initialVersions []*Version versionsToAdd []*Version expectedSize int expectedContains *Version expectedNotContain *Version }{ { name: "add to empty set", ignoreFormat: false, versionsToAdd: []*Version{ New("1.0.0", SemanticFormat), }, expectedSize: 1, expectedContains: New("1.0.0", SemanticFormat), }, { name: "add nil version", ignoreFormat: false, versionsToAdd: []*Version{ nil, }, expectedSize: 0, }, { name: "add duplicate version", ignoreFormat: false, initialVersions: []*Version{ New("1.0.0", SemanticFormat), }, versionsToAdd: []*Version{ New("1.0.0", SemanticFormat), }, expectedSize: 1, expectedContains: New("1.0.0", SemanticFormat), }, { name: "add same version different format with ignoreFormat=true", ignoreFormat: true, initialVersions: []*Version{ New("1.0.0", SemanticFormat), }, versionsToAdd: []*Version{ New("1.0.0", ApkFormat), }, expectedSize: 1, expectedContains: New("1.0.0", ApkFormat), // latest added wins }, { name: "add same version different format with ignoreFormat=false", ignoreFormat: false, initialVersions: []*Version{ New("1.0.0", SemanticFormat), }, versionsToAdd: []*Version{ New("1.0.0", ApkFormat), }, expectedSize: 2, expectedContains: New("1.0.0", SemanticFormat), }, { name: "add to set with nil versions map", ignoreFormat: false, versionsToAdd: []*Version{ New("1.0.0", SemanticFormat), }, expectedSize: 1, expectedContains: New("1.0.0", SemanticFormat), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.initialVersions...) // for testing nil versions map case if tt.name == "add to set with nil versions map" { s.versions = nil } s.Add(tt.versionsToAdd...) assert.Equal(t, tt.expectedSize, s.Size()) if tt.expectedContains != nil { assert.True(t, s.Contains(tt.expectedContains)) } if tt.expectedNotContain != nil { assert.False(t, s.Contains(tt.expectedNotContain)) } }) } } func TestSet_Remove(t *testing.T) { tests := []struct { name string ignoreFormat bool initialVersions []*Version versionsToRemove []*Version expectedSize int shouldContain *Version shouldNotContain *Version }{ { name: "remove from empty set", ignoreFormat: false, versionsToRemove: []*Version{ New("1.0.0", SemanticFormat), }, expectedSize: 0, }, { name: "remove existing version", ignoreFormat: false, initialVersions: []*Version{ New("1.0.0", SemanticFormat), New("2.0.0", SemanticFormat), }, versionsToRemove: []*Version{ New("1.0.0", SemanticFormat), }, expectedSize: 1, shouldContain: New("2.0.0", SemanticFormat), shouldNotContain: New("1.0.0", SemanticFormat), }, { name: "remove nil version", ignoreFormat: false, initialVersions: []*Version{ New("1.0.0", SemanticFormat), }, versionsToRemove: []*Version{ nil, }, expectedSize: 1, shouldContain: New("1.0.0", SemanticFormat), }, { name: "remove non-existing version", ignoreFormat: false, initialVersions: []*Version{ New("1.0.0", SemanticFormat), }, versionsToRemove: []*Version{ New("2.0.0", SemanticFormat), }, expectedSize: 1, shouldContain: New("1.0.0", SemanticFormat), }, { name: "remove from set with nil versions map", ignoreFormat: false, versionsToRemove: []*Version{ New("1.0.0", SemanticFormat), }, expectedSize: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.initialVersions...) // for testing nil versions map case if tt.name == "remove from set with nil versions map" { s.versions = nil } s.Remove(tt.versionsToRemove...) assert.Equal(t, tt.expectedSize, s.Size()) if tt.shouldContain != nil { assert.True(t, s.Contains(tt.shouldContain)) } if tt.shouldNotContain != nil { assert.False(t, s.Contains(tt.shouldNotContain)) } }) } } func TestSet_Contains(t *testing.T) { tests := []struct { name string ignoreFormat bool versions []*Version checkVersion *Version expected bool }{ { name: "contains existing version", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), New("2.0.0", SemanticFormat), }, checkVersion: New("1.0.0", SemanticFormat), expected: true, }, { name: "does not contain non-existing version", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), }, checkVersion: New("2.0.0", SemanticFormat), expected: false, }, { name: "check nil version", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), }, checkVersion: nil, expected: false, }, { name: "check version in empty set", ignoreFormat: false, versions: nil, checkVersion: New("1.0.0", SemanticFormat), expected: false, }, { name: "contains same version different format with ignoreFormat=true", ignoreFormat: true, versions: []*Version{ New("1.0.0", SemanticFormat), }, checkVersion: New("1.0.0", ApkFormat), expected: true, }, { name: "does not contain same version different format with ignoreFormat=false", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), }, checkVersion: New("1.0.0", ApkFormat), expected: false, }, { name: "check version with nil versions map", ignoreFormat: false, versions: []*Version{}, checkVersion: New("1.0.0", SemanticFormat), expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.versions...) // for testing nil versions map case if tt.name == "check version with nil versions map" { s.versions = nil } result := s.Contains(tt.checkVersion) assert.Equal(t, tt.expected, result) }) } } func TestSet_Values(t *testing.T) { tests := []struct { name string ignoreFormat bool versions []*Version expectedLength int expectedNil bool checkSorted bool }{ { name: "empty set returns nil", ignoreFormat: false, versions: nil, expectedNil: true, expectedLength: 0, }, { name: "set with versions returns sorted list", ignoreFormat: false, versions: []*Version{ New("2.0.0", SemanticFormat), New("1.0.0", SemanticFormat), New("3.0.0", SemanticFormat), }, expectedLength: 3, checkSorted: true, }, { name: "set with nil versions map returns nil", ignoreFormat: false, versions: []*Version{}, expectedNil: true, }, { name: "set with single version", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), }, expectedLength: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.versions...) // for testing nil versions map case if tt.name == "set with nil versions map returns nil" { s.versions = nil } result := s.Values() if tt.expectedNil { assert.Nil(t, result) } else { require.NotNil(t, result) assert.Equal(t, tt.expectedLength, len(result)) if tt.checkSorted && len(result) > 1 { // verify sorting - versions should be in ascending order for i := 0; i < len(result)-1; i++ { cmp, err := result[i].Compare(result[i+1]) require.NoError(t, err) assert.True(t, cmp < 0, "versions should be sorted in ascending order") } } } }) } } func TestSet_Size(t *testing.T) { tests := []struct { name string ignoreFormat bool versions []*Version expected int }{ { name: "empty set size is zero", ignoreFormat: false, versions: nil, expected: 0, }, { name: "set with versions", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), New("2.0.0", SemanticFormat), }, expected: 2, }, { name: "set with duplicate versions", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), New("1.0.0", SemanticFormat), }, expected: 1, }, { name: "set with nil versions map", ignoreFormat: false, versions: []*Version{}, expected: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.versions...) // for testing nil versions map case if tt.name == "set with nil versions map" { s.versions = nil } result := s.Size() assert.Equal(t, tt.expected, result) }) } } func TestSet_Clear(t *testing.T) { tests := []struct { name string ignoreFormat bool versions []*Version }{ { name: "clear non-empty set", ignoreFormat: false, versions: []*Version{ New("1.0.0", SemanticFormat), New("2.0.0", SemanticFormat), }, }, { name: "clear empty set", ignoreFormat: false, versions: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := NewSet(tt.ignoreFormat, tt.versions...) originalSize := s.Size() s.Clear() assert.Equal(t, 0, s.Size()) assert.NotNil(t, s.versions) // should have empty map, not nil // verify all previous versions are gone if originalSize > 0 { for _, v := range tt.versions { if v != nil { assert.False(t, s.Contains(v)) } } } }) } } func TestSet_Integration(t *testing.T) { // test combining multiple operations s := NewSet(false) v1 := New("1.0.0", SemanticFormat) v2 := New("2.0.0", SemanticFormat) v3 := New("3.0.0", SemanticFormat) // add versions s.Add(v1, v2, v3) assert.Equal(t, 3, s.Size()) // check contains assert.True(t, s.Contains(v1)) assert.True(t, s.Contains(v2)) assert.True(t, s.Contains(v3)) // remove one version s.Remove(v2) assert.Equal(t, 2, s.Size()) assert.False(t, s.Contains(v2)) // get values values := s.Values() require.Len(t, values, 2) // verify sorting cmp, err := values[0].Compare(values[1]) require.NoError(t, err) assert.True(t, cmp < 0) // clear all s.Clear() assert.Equal(t, 0, s.Size()) assert.Nil(t, s.Values()) } ================================================ FILE: grype/version/version.go ================================================ package version import ( "errors" "fmt" ) var _ Comparator = (*Version)(nil) type Version struct { Raw string Format Format comparators map[Format]Comparator Config ComparisonConfig } // New creates a new Version with the default comparison configuration. // The default uses no explicit MissingEpochStrategy, which preserves the native // behavior of each format's comparator (RPM ignores one-sided epochs, DEB treats // missing as 0). Use NewWithConfig to explicitly control epoch handling. func New(raw string, format Format) *Version { return NewWithConfig(raw, format, ComparisonConfig{}) } // NewWithConfig creates a new Version with a specific comparison configuration. // This allows control over how missing epochs are handled during version comparison. func NewWithConfig(raw string, format Format, cfg ComparisonConfig) *Version { return &Version{ Raw: raw, Format: format, Config: cfg, } } func (v *Version) Validate() error { _, err := v.getComparator(v.Format) return err } //nolint:funlen func (v *Version) getComparator(format Format) (Comparator, error) { if v.comparators == nil { v.comparators = make(map[Format]Comparator) } if comparator, ok := v.comparators[format]; ok { return comparator, nil } var comparator Comparator var err error switch format { case SemanticFormat: // not enforcing strict semver here, so that we can parse versions like "v1.0.0", "1.0", or "1.0a", which aren't strictly semver compliant comparator, err = newSemanticVersion(v.Raw, false) case ApkFormat: comparator, err = newApkVersion(v.Raw) case BitnamiFormat: comparator, err = newBitnamiVersion(v.Raw) case DebFormat: comparator, err = newDebVersion(v.Raw) case GolangFormat: comparator, err = newGolangVersion(v.Raw) case MavenFormat: comparator, err = newMavenVersion(v.Raw) case RpmFormat: comparator, err = newRpmVersion(v.Raw) case PythonFormat: comparator, err = newPep440Version(v.Raw) case KBFormat: comparator = newKBVersion(v.Raw) case GemFormat: comparator, err = newGemVersion(v.Raw) case PortageFormat: comparator = newPortageVersion(v.Raw) case JVMFormat: comparator, err = newJvmVersion(v.Raw) case PacmanFormat: comparator, err = newPacmanVersion(v.Raw) case UnknownFormat: comparator, err = newFuzzyVersion(v.Raw) default: err = fmt.Errorf("no comparator available for format %q", v.Format) } v.comparators[format] = comparator return comparator, err } func (v Version) String() string { return fmt.Sprintf("%s (%s)", v.Raw, v.Format) } // Compare compares this version to another version. // This returns -1, 0, or 1 if this version is smaller, // equal, or larger than the other version, respectively. func (v Version) Compare(other *Version) (int, error) { if other == nil { return -1, ErrNoVersionProvided } var result int comparator, err := v.getComparator(v.Format) if err == nil { // if the package version, v was able to compare without error, return the result result, err = comparator.Compare(other) if err == nil { // no error returned for package version or db version, return the result return result, nil } } // we were unable to parse the package or db version as v.Format, try other.Format if they differ if v.Format != other.Format { originalErr := err comparator, err = v.getComparator(other.Format) if err == nil { result, err = comparator.Compare(other) if err == nil { return result, nil } } err = errors.Join(originalErr, err) } // all formats returned error, return all errors return 0, fmt.Errorf("unable to compare versions: %v %v due to %w", v, other, err) } func (v *Version) Is(op Operator, other *Version) (bool, error) { if v == nil { return false, fmt.Errorf("cannot evaluate version with nil version") } if other == nil { return false, ErrNoVersionProvided } comparator, err := v.getComparator(v.Format) if err != nil { return false, fmt.Errorf("unable to get comparator for %s: %w", v.Format, err) } result, err := comparator.Compare(other) if err != nil { return false, fmt.Errorf("unable to compare versions %s and %s: %w", v, other, err) } switch op { case EQ, "": return result == 0, nil case GT: return result > 0, nil case LT: return result < 0, nil case GTE: return result >= 0, nil case LTE: return result <= 0, nil } return false, fmt.Errorf("unknown operator %s", op) } ================================================ FILE: grype/version/version_test.go ================================================ package version import ( "slices" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestVersionCompare(t *testing.T) { tests := []struct { name string v1 string v2 string expectedResult int expectErr require.ErrorAssertionFunc }{ { name: "v1 greater than v2", v1: "2.0.0", v2: "1.0.0", expectedResult: 1, }, { name: "v1 less than v2", v1: "1.0.0", v2: "2.0.0", expectedResult: -1, }, { name: "v1 equal to v2", v1: "1.0.0", v2: "1.0.0", expectedResult: 0, }, { name: "compare with nil version", v1: "1.0.0", v2: "", expectedResult: -1, expectErr: require.Error, }, } // the above test cases are pretty tame value-wise, so we can use (almost) all formats var formats []Format formats = append(formats, Formats...) // leave out some formats... slices.DeleteFunc(formats, func(f Format) bool { return f == KBFormat }) for _, format := range formats { t.Run(format.String(), func(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.expectErr == nil { tc.expectErr = require.NoError } v1 := New(tc.v1, format) require.Equal(t, format, v1.Format) var v2 *Version if tc.v2 != "" { v2 = New(tc.v2, format) require.Equal(t, format, v2.Format) } result, err := v1.Compare(v2) tc.expectErr(t, err, "unexpected error during comparison") if err != nil { return // skip further checks if there was an error } assert.NoError(t, err, "unexpected error during comparison") assert.Equal(t, tc.expectedResult, result, "comparison result mismatch") }) } }) } } func TestVersion_UpgradeUnknownRightSideComparison(t *testing.T) { v1 := New("1.0.0", SemanticFormat) // test if we can upgrade an unknown format to a known format when the left hand side is known v2 := New("1.0.0", UnknownFormat) result, err := v1.Compare(v2) assert.NoError(t, err) assert.Equal(t, 0, result, "versions should be equal after format conversion") } func TestVersionCompareSameFormat(t *testing.T) { formats := []struct { name string format Format }{ {"Semantic", SemanticFormat}, {"APK", ApkFormat}, {"Deb", DebFormat}, {"Golang", GolangFormat}, {"Maven", MavenFormat}, {"RPM", RpmFormat}, {"Python", PythonFormat}, {"KB", KBFormat}, {"Gem", GemFormat}, {"Portage", PortageFormat}, {"JVM", JVMFormat}, {"Unknown", UnknownFormat}, } for _, fmt := range formats { t.Run(fmt.name, func(t *testing.T) { // just test that we can create and compare versions of this format // without errors - not testing the actual comparison logic v1 := New("1.0.0", fmt.format) v2 := New("1.0.0", fmt.format) result, err := v1.Compare(v2) assert.NoError(t, err, "comparison error") assert.Equal(t, 0, result, "equal versions should return 0") }) } } func TestVersion_Is(t *testing.T) { tests := []struct { name string version *Version operator Operator other *Version expected bool wantErr require.ErrorAssertionFunc }{ { name: "equal versions - EQ operator", version: New("1.0.0", SemanticFormat), operator: EQ, other: New("1.0.0", SemanticFormat), expected: true, }, { name: "equal versions - empty operator (defaults to EQ)", version: New("1.0.0", SemanticFormat), operator: "", other: New("1.0.0", SemanticFormat), expected: true, }, { name: "unequal versions - EQ operator", version: New("1.0.0", SemanticFormat), operator: EQ, other: New("2.0.0", SemanticFormat), expected: false, }, { name: "greater than - GT operator true", version: New("2.0.0", SemanticFormat), operator: GT, other: New("1.0.0", SemanticFormat), expected: true, }, { name: "greater than - GT operator false", version: New("1.0.0", SemanticFormat), operator: GT, other: New("2.0.0", SemanticFormat), expected: false, }, { name: "greater than or equal - GTE operator true (greater)", version: New("2.0.0", SemanticFormat), operator: GTE, other: New("1.0.0", SemanticFormat), expected: true, }, { name: "greater than or equal - GTE operator true (equal)", version: New("1.0.0", SemanticFormat), operator: GTE, other: New("1.0.0", SemanticFormat), expected: true, }, { name: "greater than or equal - GTE operator false", version: New("1.0.0", SemanticFormat), operator: GTE, other: New("2.0.0", SemanticFormat), expected: false, }, { name: "less than - LT operator true", version: New("1.0.0", SemanticFormat), operator: LT, other: New("2.0.0", SemanticFormat), expected: true, }, { name: "less than - LT operator false", version: New("2.0.0", SemanticFormat), operator: LT, other: New("1.0.0", SemanticFormat), expected: false, }, { name: "less than or equal - LTE operator true (less)", version: New("1.0.0", SemanticFormat), operator: LTE, other: New("2.0.0", SemanticFormat), expected: true, }, { name: "less than or equal - LTE operator true (equal)", version: New("1.0.0", SemanticFormat), operator: LTE, other: New("1.0.0", SemanticFormat), expected: true, }, { name: "less than or equal - LTE operator false", version: New("2.0.0", SemanticFormat), operator: LTE, other: New("1.0.0", SemanticFormat), expected: false, }, { name: "nil other version should return ErrNoVersionProvided", version: New("1.0.0", SemanticFormat), operator: EQ, other: nil, wantErr: require.Error, }, { name: "unknown operator should return error", version: New("1.0.0", SemanticFormat), operator: "unknown", other: New("1.0.0", SemanticFormat), wantErr: require.Error, }, { name: "invalid version format should return error", version: New("invalid", SemanticFormat), operator: EQ, other: New("1.0.0", SemanticFormat), wantErr: require.Error, }, { name: "different formats - semantic vs apk", version: New("1.0.0", SemanticFormat), operator: EQ, other: New("1.0.0", ApkFormat), expected: true, }, { name: "complex semantic versions", version: New("1.2.3-alpha.1", SemanticFormat), operator: LT, other: New("1.2.3", SemanticFormat), expected: true, }, { name: "version with v prefix", version: New("v1.0.0", SemanticFormat), operator: EQ, other: New("1.0.0", SemanticFormat), expected: true, }, { name: "nil other version is ErrNoVersionProvided", version: New("1.0.0", SemanticFormat), operator: EQ, other: nil, wantErr: func(t require.TestingT, err error, a ...interface{}) { require.ErrorIs(t, err, ErrNoVersionProvided, a...) }, }, { name: "unknown operator error", version: New("1.0.0", SemanticFormat), operator: "!@#", other: New("1.0.0", SemanticFormat), wantErr: func(t require.TestingT, err error, a ...interface{}) { require.ErrorContains(t, err, "unknown operator !@#", a...) }, }, { name: "invalid version format error contains format", version: New("not-a-valid-version", SemanticFormat), operator: EQ, other: New("1.0.0", SemanticFormat), wantErr: func(t require.TestingT, err error, a ...interface{}) { require.ErrorContains(t, err, "unable to get comparator for Semantic", a...) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } result, err := tt.version.Is(tt.operator, tt.other) tt.wantErr(t, err) if err != nil { return } assert.Equal(t, tt.expected, result) }) } } func TestVersion_Is_AllOperators(t *testing.T) { v1 := New("1.0.0", SemanticFormat) v2 := New("2.0.0", SemanticFormat) v1dup := New("1.0.0", SemanticFormat) tests := []struct { name string left *Version operator Operator right *Version expected bool }{ // v1 (1.0.0) vs v2 (2.0.0) {"1.0.0 = 2.0.0", v1, EQ, v2, false}, {"1.0.0 > 2.0.0", v1, GT, v2, false}, {"1.0.0 >= 2.0.0", v1, GTE, v2, false}, {"1.0.0 < 2.0.0", v1, LT, v2, true}, {"1.0.0 <= 2.0.0", v1, LTE, v2, true}, // v2 (2.0.0) vs v1 (1.0.0) {"2.0.0 = 1.0.0", v2, EQ, v1, false}, {"2.0.0 > 1.0.0", v2, GT, v1, true}, {"2.0.0 >= 1.0.0", v2, GTE, v1, true}, {"2.0.0 < 1.0.0", v2, LT, v1, false}, {"2.0.0 <= 1.0.0", v2, LTE, v1, false}, // v1 (1.0.0) vs v1dup (1.0.0) {"1.0.0 = 1.0.0", v1, EQ, v1dup, true}, {"1.0.0 > 1.0.0", v1, GT, v1dup, false}, {"1.0.0 >= 1.0.0", v1, GTE, v1dup, true}, {"1.0.0 < 1.0.0", v1, LT, v1dup, false}, {"1.0.0 <= 1.0.0", v1, LTE, v1dup, true}, // empty operator should default to EQ {"1.0.0 (empty) 1.0.0", v1, "", v1dup, true}, {"1.0.0 (empty) 2.0.0", v1, "", v2, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := tt.left.Is(tt.operator, tt.right) require.NoError(t, err) assert.Equal(t, tt.expected, result) }) } } ================================================ FILE: grype/vex/csaf/csaf.go ================================================ package csaf import ( "slices" "github.com/gocsaf/csaf/v3/csaf" ) // advisoryMatch captures the criteria that caused a vulnerability to match a CSAF advisory type advisoryMatch struct { Vulnerability *csaf.Vulnerability Status status ProductID csaf.ProductID } // cve returns the CVE of the vulnerability that matched func (m *advisoryMatch) cve() string { if m == nil || m.Vulnerability == nil || m.Vulnerability.CVE == nil { return "" } return string(*m.Vulnerability.CVE) } // statement returns the statement of the vulnerability that matched func (m *advisoryMatch) statement() string { if m == nil || m.Vulnerability == nil { return "" } // an impact statement SHALL exist as machine readable flag in /vulnerabilities[]/flags (...) for _, flag := range m.Vulnerability.Flags { if flag == nil || flag.ProductIds == nil || flag.Label == nil { continue } for _, pID := range *flag.ProductIds { if pID == nil { continue } if *pID == m.ProductID { return string(*flag.Label) } } } // (...) or as human readable justification in /vulnerabilities[]/threats for _, th := range m.Vulnerability.Threats { if th == nil || th.Category == nil || th.Details == nil { continue } if *th.Category != csaf.CSAFThreatCategoryImpact { continue } for _, pID := range *th.ProductIds { if pID == nil { continue } if *pID == m.ProductID { return *th.Details } } } return "" } type advisories []*csaf.Advisory // matches returns the first CSAF advisory to match for a given vulnerability ID and package URL // //nolint:gocognit func (advisories advisories) matches(vulnID, purl string) *advisoryMatch { for _, adv := range advisories { if adv == nil || adv.Vulnerabilities == nil { continue } // Auxiliary function to find in the advisory the 1st product ID that matches a given pURL findProductID := func(products csaf.Products, purl string) csaf.ProductID { for _, p := range products { if p == nil { continue } if slices.Contains(purlsFromProductIdentificationHelpers(adv.ProductTree.CollectProductIdentificationHelpers(*p)), purl) { return *p } } return "" } for _, vuln := range adv.Vulnerabilities { if vuln == nil || vuln.CVE == nil || string(*vuln.CVE) != vulnID { continue } productsByStatus := map[status]*csaf.Products{ firstAffected: vuln.ProductStatus.FirstAffected, firstFixed: vuln.ProductStatus.FirstFixed, fixed: vuln.ProductStatus.Fixed, knownAffected: vuln.ProductStatus.KnownAffected, knownNotAffected: vuln.ProductStatus.KnownNotAffected, lastAffected: vuln.ProductStatus.LastAffected, recommended: vuln.ProductStatus.Recommended, underInvestigation: vuln.ProductStatus.UnderInvestigation, } for status, products := range productsByStatus { if products == nil { continue } if productID := findProductID(*products, purl); productID != "" { return &advisoryMatch{vuln, status, productID} } } } } return nil } // purlsFromProductIdentificationHelpers returns a slice of PackageURLs (string format) given a slice of ProductIdentificationHelpers. func purlsFromProductIdentificationHelpers(helpers []*csaf.ProductIdentificationHelper) []string { var purls []string for _, helper := range helpers { if helper == nil || helper.PURL == nil { continue } purls = append(purls, string(*helper.PURL)) } return purls } ================================================ FILE: grype/vex/csaf/csaf_test.go ================================================ package csaf import ( "reflect" "testing" "github.com/gocsaf/csaf/v3/csaf" ) func Test_advisoryMatch_statement(t *testing.T) { type fields struct { Vulnerability *csaf.Vulnerability ProductID csaf.ProductID } tests := []struct { name string fields fields want string }{ { name: "no vulnerability", fields: fields{ Vulnerability: nil, ProductID: "SPB-00260", }, want: "", }, { name: "no flags or threats", fields: fields{ Vulnerability: &csaf.Vulnerability{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], }, ProductID: "SPB-00260", }, want: "", }, { name: "flag with label", fields: fields{ Vulnerability: &csaf.Vulnerability{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], Flags: []*csaf.Flag{{ ProductIds: &csaf.Products{&[]csaf.ProductID{"SPB-00260"}[0]}, Label: &[]csaf.FlagLabel{"vulnerable_code_not_present"}[0], }}, }, ProductID: "SPB-00260", }, want: "vulnerable_code_not_present", }, { name: "flag with label, different product ID", fields: fields{ Vulnerability: &csaf.Vulnerability{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], Flags: []*csaf.Flag{{ ProductIds: &csaf.Products{&[]csaf.ProductID{"SPB-00260"}[0]}, Label: &[]csaf.FlagLabel{"vulnerable_code_not_present"}[0], }}, }, ProductID: "SPB-00261", }, want: "", }, { name: "threat with details", fields: fields{ Vulnerability: &csaf.Vulnerability{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], Threats: []*csaf.Threat{{ Category: &[]csaf.ThreatCategory{csaf.CSAFThreatCategoryImpact}[0], Details: &[]string{"Class with vulnerable code was removed before shipping"}[0], ProductIds: &csaf.Products{&[]csaf.ProductID{"SPB-00260"}[0]}, }}, }, ProductID: "SPB-00260", }, want: "Class with vulnerable code was removed before shipping", }, { name: "threat with details, different product ID", fields: fields{ Vulnerability: &csaf.Vulnerability{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], Threats: []*csaf.Threat{{ Category: &[]csaf.ThreatCategory{csaf.CSAFThreatCategoryImpact}[0], Details: &[]string{"Class with vulnerable code was removed before shipping"}[0], ProductIds: &csaf.Products{&[]csaf.ProductID{"SPB-00260"}[0]}, }}, }, ProductID: "SPB-00261", }, want: "", }, } t.Parallel() for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() m := &advisoryMatch{ Vulnerability: test.fields.Vulnerability, ProductID: test.fields.ProductID, } if got := m.statement(); got != test.want { tt.Errorf("advisoryMatch.statement() = %v, want %v", got, test.want) } }) } } func Test_advisories_matches(t *testing.T) { sampleAdv := &csaf.Advisory{ ProductTree: &csaf.ProductTree{ Branches: csaf.Branches{ &[]csaf.Branch{{ Branches: csaf.Branches{ &[]csaf.Branch{{ Category: &[]csaf.BranchCategory{csaf.CSAFBranchCategoryProductVersion}[0], Name: &[]string{"2.6.0"}[0], Product: &csaf.FullProductName{ Name: &[]string{"Spring Boot 2.6.0"}[0], ProductID: &[]csaf.ProductID{"SPB-00260"}[0], ProductIdentificationHelper: &[]csaf.ProductIdentificationHelper{{ PURL: &[]csaf.PURL{"pkg:apk/alpine/libssl3@3.0.8-r3"}[0], }}[0], }, }}[0], }, Category: &[]csaf.BranchCategory{csaf.CSAFBranchCategoryProductName}[0], Name: &[]string{"Spring"}[0], }}[0], }, }, Vulnerabilities: []*csaf.Vulnerability{{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], ProductStatus: &[]csaf.ProductStatus{{ KnownNotAffected: &csaf.Products{ &[]csaf.ProductID{"SPB-00260"}[0], }, }}[0], }}, } type args struct { vulnID string purl string } tests := []struct { name string advisories advisories args args want *advisoryMatch }{ { name: "no advisories", advisories: advisories{}, args: args{vulnID: "CVE-1234-5678", purl: "pkg:apk/alpine/libssl3@3.0.8-r3"}, want: nil, }, { name: "no matching advisory", advisories: advisories{sampleAdv}, args: args{vulnID: "CVE-1234-5678", purl: "pkg:apk/alpine/libcrypto3@3.0.8-r3"}, want: nil, }, { name: "advisory matches vulnerability for given pURL", advisories: advisories{sampleAdv}, args: args{vulnID: "CVE-1234-5678", purl: "pkg:apk/alpine/libssl3@3.0.8-r3"}, want: &advisoryMatch{ Vulnerability: &csaf.Vulnerability{ CVE: &[]csaf.CVE{"CVE-1234-5678"}[0], ProductStatus: &[]csaf.ProductStatus{{ KnownNotAffected: &csaf.Products{ &[]csaf.ProductID{"SPB-00260"}[0], }, }}[0], }, ProductID: "SPB-00260", Status: knownNotAffected, }, }, } t.Parallel() for _, testToRun := range tests { test := testToRun t.Run(test.name, func(tt *testing.T) { tt.Parallel() if got := test.advisories.matches(test.args.vulnID, test.args.purl); !reflect.DeepEqual(got, test.want) { tt.Errorf("advisories.matches() = %v, want %v", got, test.want) } }) } } ================================================ FILE: grype/vex/csaf/implementation.go ================================================ package csaf import ( "errors" "fmt" "slices" "time" "github.com/gocsaf/csaf/v3/csaf" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" vexStatus "github.com/anchore/grype/grype/vex/status" ) // searchedBy captures the parameters used to search through the VEX data type searchedBy struct { Vulnerability string Purl string } type Processor struct{} func New() *Processor { return &Processor{} } // IsCSAF checks if the provided document is a CSAF document func IsCSAF(document string) bool { if _, err := csaf.LoadAdvisory(document); err == nil { return true } return false } // ReadVexDocuments reads different files and creates a collection of advisories based on them. func (*Processor) ReadVexDocuments(docs []string) (interface{}, error) { var advs advisories for _, doc := range docs { adv, err := csaf.LoadAdvisory(doc) if err != nil { return nil, fmt.Errorf("error loading VEX CSAF document: %w", err) } advs = append(advs, adv) } slices.SortStableFunc(advs, newerCurrentReleaseDateFirst) return advs, nil } // newerCurrentReleaseDateFirst compares csaf.Advisories by the document.Tracking.CurrentReleaseDate // field, treating newer dates as earlier values, and nil and invalid dates as later than // all valid dates func newerCurrentReleaseDateFirst(a, b *csaf.Advisory) int { parseDate := func(datePtr *string) (time.Time, bool) { if datePtr == nil { return time.Time{}, false } t, err := time.Parse(time.RFC3339, *datePtr) return t, err == nil } aT, aValid := parseDate(a.Document.Tracking.CurrentReleaseDate) bT, bValid := parseDate(b.Document.Tracking.CurrentReleaseDate) switch { case !aValid && !bValid: return 0 case !bValid: return -1 case !aValid: return 1 default: return bT.Compare(aT) // newer first } } // FilterMatches takes a set of scanning results and moves any results marked in // the VEX data as fixed or not_affected to the ignored list. func (*Processor) FilterMatches( docRaw interface{}, ignoreRules []match.IgnoreRule, _ *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch, ) (*match.Matches, []match.IgnoredMatch, error) { advisories, ok := docRaw.(advisories) if !ok { return nil, nil, errors.New("unable to cast vex document as CSAF Advisories") } remainingMatches := match.NewMatches() for _, m := range matches.Sorted() { // Seek if our advisories have information about a vulnerability affecting // the product for which we have a match. advMatch := advisories.matches(m.Vulnerability.ID, m.Package.PURL) if advMatch == nil { remainingMatches.Add(m) continue } // Filtering only applies to not_affected and fixed statuses if !matchesVexStatus(advMatch.Status, vexStatus.NotAffected) && !matchesVexStatus(advMatch.Status, vexStatus.Fixed) { remainingMatches.Add(m) continue } // Check if there's any ignore rule that matches the current match statement rule := matchingRule(ignoreRules, m, advMatch, vexStatus.IgnoreList()) if rule == nil { remainingMatches.Add(m) continue } ignoredMatches = append(ignoredMatches, match.IgnoredMatch{ Match: m, AppliedIgnoreRules: []match.IgnoreRule{*rule}, }) } return &remainingMatches, ignoredMatches, nil } // AugmentMatches adds results to the match.Matches array when matching data // about an affected VEX product is found on loaded VEX documents. Matches // are moved from the ignore list back to active matches. func (*Processor) AugmentMatches( docRaw interface{}, ignoreRules []match.IgnoreRule, _ *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch, ) (*match.Matches, []match.IgnoredMatch, error) { advisories, ok := docRaw.(advisories) if !ok { return nil, nil, errors.New("unable to cast vex document as CSAF Advisories") } remainingIgnoredMatches := []match.IgnoredMatch{} for _, m := range ignoredMatches { if advMatch := advisories.matches(m.Vulnerability.ID, m.Package.PURL); advMatch != nil { if rule := matchingRule(ignoreRules, m.Match, advMatch, vexStatus.AugmentList()); rule != nil { newMatch := m.Match newMatch.Details = append(newMatch.Details, match.Detail{ Type: match.ExactDirectMatch, SearchedBy: &searchedBy{ Vulnerability: m.Vulnerability.ID, Purl: m.Package.PURL, }, Found: advMatch, Matcher: match.CsafVexMatcher, }) matches.Add(newMatch) continue } } remainingIgnoredMatches = append(remainingIgnoredMatches, m) } return matches, remainingIgnoredMatches, nil } // matchingRule cycles through a set of ignore rules and returns the first // one that matches the statement and the match. Returns nil if none match. func matchingRule(ignoreRules []match.IgnoreRule, m match.Match, advMatch *advisoryMatch, allowedStatuses []vexStatus.Status) *match.IgnoreRule { ms := match.NewMatches() ms.Add(m) // By default, if there are no ignore rules (which means the user didn't provide // any custom VEX rule), a matching rule should be returned if the advisory // match status is one of the allowed statuses. if len(ignoreRules) == 0 { for _, status := range allowedStatuses { if matchesVexStatus(advMatch.Status, status) { return &match.IgnoreRule{ Namespace: "vex", Vulnerability: advMatch.cve(), VexJustification: advMatch.statement(), VexStatus: string(status), } } } } for _, rule := range ignoreRules { // If the rule has more conditions than just the VEX statement, check if // it applies to the current match. if rule.HasConditions() { r := rule r.VexStatus = "" if _, ignored := match.ApplyIgnoreRules(ms, []match.IgnoreRule{r}); len(ignored) == 0 { continue } } // If the advisory match status is not the same as the rule status, // it does not apply if !matchesVexStatus(advMatch.Status, vexStatus.Status(rule.VexStatus)) { continue } // If the rule has a status other than the allowed ones, skip: if rule.VexStatus != "" && !slices.Contains(allowedStatuses, vexStatus.Status(rule.VexStatus)) { continue } // If the vulnerability is blank in the rule it means we will honor // any status with any vulnerability. Alternatively, if the vulnerability // is set, the rule applies if it is the same in the advisory match and the rule. if rule.Vulnerability == "" || advMatch.cve() == rule.Vulnerability { return &rule } // If the rule applies to a VEX justification it needs to match the // advisory match statement, note that justifications only apply to not_affected: if matchesVexStatus(advMatch.Status, vexStatus.NotAffected) && rule.VexJustification != "" && rule.VexJustification != advMatch.statement() { continue } if advMatch.cve() == rule.Vulnerability { return &rule } } return nil } ================================================ FILE: grype/vex/csaf/implementation_test.go ================================================ package csaf import ( "slices" "testing" "github.com/gocsaf/csaf/v3/csaf" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" vexStatus "github.com/anchore/grype/grype/vex/status" "github.com/anchore/grype/grype/vulnerability" ) func Test_newerCurrentReleaseDateFirst(t *testing.T) { type dateIDPair struct { date string id string } tests := []struct { name string input []dateIDPair expected []string }{ { name: "simple sort newest first", input: []dateIDPair{ {"2023-01-01T00:00:00Z", "doc1"}, {"2024-01-01T00:00:00Z", "doc2"}, {"2022-01-01T00:00:00Z", "doc3"}, }, expected: []string{"doc2", "doc1", "doc3"}, }, { name: "already sorted", input: []dateIDPair{ {"2024-01-01T00:00:00Z", "doc1"}, {"2023-01-01T00:00:00Z", "doc2"}, }, expected: []string{"doc1", "doc2"}, }, { name: "same dates maintain order", input: []dateIDPair{ {"2023-01-01T00:00:00Z", "first"}, {"2023-01-01T00:00:00Z", "second"}, }, expected: []string{"first", "second"}, }, { name: "nil dates go last", input: []dateIDPair{ {"", "nil1"}, {"2023-01-01T00:00:00Z", "valid1"}, {"2024-01-01T00:00:00Z", "valid2"}, }, expected: []string{"valid2", "valid1", "nil1"}, }, { name: "multiple nils maintain order", input: []dateIDPair{ {"", "nil1"}, {"2023-01-01T00:00:00Z", "valid"}, {"", "nil2"}, }, expected: []string{"valid", "nil1", "nil2"}, }, { name: "all nils", input: []dateIDPair{ {"", "first"}, {"", "second"}, {"", "third"}, }, expected: []string{"first", "second", "third"}, }, { name: "invalid date format goes last", input: []dateIDPair{ {"invalid-date", "bad"}, {"2023-01-01T00:00:00Z", "good"}, }, expected: []string{"good", "bad"}, }, { name: "mix of nil invalid and valid", input: []dateIDPair{ {"", "nil"}, {"invalid", "bad"}, {"2024-01-01T00:00:00Z", "new"}, {"2023-01-01T00:00:00Z", "old"}, }, expected: []string{"new", "old", "nil", "bad"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { advs := make(advisories, len(tt.input)) for i, pair := range tt.input { var datePtr *string if pair.date == "" { datePtr = nil } else { datePtr = &pair.date } advs[i] = &csaf.Advisory{ Document: &csaf.Document{ Tracking: &csaf.Tracking{ ID: (*csaf.TrackingID)(&pair.id), CurrentReleaseDate: datePtr, }, }, } } slices.SortStableFunc(advs, newerCurrentReleaseDateFirst) result := make([]string, len(advs)) for i, adv := range advs { result[i] = string(*adv.Document.Tracking.ID) } assert.Equal(t, tt.expected, result) }) } } func Test_matchingRule(t *testing.T) { tests := []struct { name string ignoreRules []match.IgnoreRule m match.Match advMatch *advisoryMatch allowedStatuses []vexStatus.Status expected *match.IgnoreRule }{ { name: "no ignore rules, not_affected status with inline mitigations", ignoreRules: []match.IgnoreRule{}, // No existing ignore rules m: match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2023-1234"}, }, Package: pkg.Package{Name: "test-package"}, }, advMatch: &advisoryMatch{ Vulnerability: &csaf.Vulnerability{ CVE: func() *csaf.CVE { cve := csaf.CVE("CVE-2023-1234"); return &cve }(), }, Status: knownNotAffected, // CSAF status ProductID: "test-product-1", }, allowedStatuses: vexStatus.IgnoreList(), // [Fixed, NotAffected] expected: &match.IgnoreRule{ Namespace: "vex", Vulnerability: "CVE-2023-1234", VexJustification: "", // Will be empty since no flags/threats in this simple case VexStatus: "not_affected", }, }, { name: "no ignore rules, under_investigation status should return nil", ignoreRules: []match.IgnoreRule{}, // No existing ignore rules m: match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ID: "CVE-2023-5678"}, }, Package: pkg.Package{Name: "another-package"}, }, advMatch: &advisoryMatch{ Vulnerability: &csaf.Vulnerability{ CVE: func() *csaf.CVE { cve := csaf.CVE("CVE-2023-5678"); return &cve }(), }, Status: underInvestigation, // CSAF status ProductID: "test-product-2", }, allowedStatuses: vexStatus.IgnoreList(), // [Fixed, NotAffected] - doesn't include UnderInvestigation expected: nil, // Should return nil since under_investigation is not in allowed statuses }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := matchingRule(tt.ignoreRules, tt.m, tt.advMatch, tt.allowedStatuses) if tt.expected == nil { require.Nil(t, result) return } if d := cmp.Diff(*result, *tt.expected); d != "" { t.Errorf("mismatch (-want +got):\n%s", d) } }) } } ================================================ FILE: grype/vex/csaf/status.go ================================================ package csaf import vexStatus "github.com/anchore/grype/grype/vex/status" type status string const ( firstAffected status = "first_affected" firstFixed status = "first_fixed" fixed status = "fixed" knownAffected status = "known_affected" knownNotAffected status = "known_not_affected" lastAffected status = "last_affected" recommended status = "recommended" underInvestigation status = "under_investigation" ) // matchesVexStatus returns true if the given CSAF status matches the given VEX status. func matchesVexStatus(csafStatus status, status vexStatus.Status) bool { // CSAF implementation has slightly different, richer statuses than the original VEX proposed by CISA switch csafStatus { case firstAffected, knownAffected, lastAffected, recommended: return status == vexStatus.Affected case firstFixed, fixed: return status == vexStatus.Fixed case knownNotAffected: return status == vexStatus.NotAffected case underInvestigation: return status == vexStatus.UnderInvestigation default: return false } } ================================================ FILE: grype/vex/openvex/implementation.go ================================================ package openvex import ( "errors" "fmt" "slices" "strings" "github.com/google/go-containerregistry/pkg/name" openvex "github.com/openvex/go-vex/pkg/vex" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" vexStatus "github.com/anchore/grype/grype/vex/status" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/source" ) type Processor struct{} func New() *Processor { return &Processor{} } // Match captures the criteria that caused a vulnerability to match type Match struct { Statement openvex.Statement } // SearchedBy captures the parameters used to search through the VEX data type SearchedBy struct { Vulnerability string Product string Subcomponents []string } // IsOpenVex checks if the provided document is a VEX document func IsOpenVex(document string) bool { if _, err := openvex.Load(document); err == nil { return true } return false } // ReadVexDocuments reads and merges VEX documents func (ovm *Processor) ReadVexDocuments(docs []string) (interface{}, error) { // Combine all VEX documents into a single VEX document vexdata, err := openvex.MergeFiles(docs) if err != nil { return nil, fmt.Errorf("merging vex documents: %w", err) } return vexdata, nil } // productIdentifiersFromContext reads the package context and returns software // identifiers identifying the scanned image. func productIdentifiersFromContext(pkgContext *pkg.Context) []string { switch v := pkgContext.Source.Metadata.(type) { case source.ImageMetadata: tagIdentifiers := identifiersFromTags(v.Tags, pkgContext.Source.Name) digestIdentifiers := identifiersFromDigests(v.RepoDigests) identifiers := slices.Concat(tagIdentifiers, digestIdentifiers) return identifiers default: if pkgContext.Source.Name != "" && pkgContext.Source.Version != "" { return []string{"pkg:generic/" + strings.ToLower(pkgContext.Source.Name) + "@" + pkgContext.Source.Version} } // return an empty list so matching can be attempted using the // package's own identifiers as the product return []string{} } } func normalizeDockerHubRepositoryURL(repoURL string) string { repoURL = strings.TrimSpace(repoURL) if repoURL == "" { return repoURL } repoURL = strings.TrimPrefix(repoURL, "https://") repoURL = strings.TrimPrefix(repoURL, "http://") repoURL = strings.TrimSuffix(repoURL, "/") host, rest, hasSlash := strings.Cut(repoURL, "/") switch strings.ToLower(host) { case "docker.io", "index.docker.io", "registry-1.docker.io": host = "index.docker.io" } if !hasSlash || rest == "" { return host } return host + "/" + rest } func identifiersFromTags(tags []string, name string) []string { identifiers := []string{} for _, tag := range tags { identifiers = append(identifiers, tag) tagMap := map[string]string{} _, splitTag, found := strings.Cut(tag, ":") if found { tagMap["tag"] = splitTag qualifiers := packageurl.QualifiersFromMap(tagMap) identifiers = append(identifiers, packageurl.NewPackageURL("oci", "", name, "", qualifiers, "").String()) } } return identifiers } func identifiersFromDigests(digests []string) []string { identifiers := []string{} for _, d := range digests { // The first identifier is the original image reference: identifiers = append(identifiers, d) // Not an image reference, skip ref, err := name.ParseReference(d) if err != nil { continue } var repoURL string shaString := ref.Identifier() // If not a digest, we can't form a purl, so skip it if !strings.HasPrefix(shaString, "sha256:") { continue } pts := strings.Split(ref.Context().RepositoryStr(), "/") name := pts[len(pts)-1] repoURL = strings.TrimSuffix( ref.Context().RegistryStr()+"/"+ref.Context().RepositoryStr(), fmt.Sprintf("/%s", name), ) repoURL = normalizeDockerHubRepositoryURL(repoURL) qMap := map[string]string{} if repoURL != "" { qMap["repository_url"] = repoURL } qs := packageurl.QualifiersFromMap(qMap) identifiers = append(identifiers, packageurl.NewPackageURL( "oci", "", name, shaString, qs, "", ).String()) // Add a hash to the identifier list in case people want to vex // using the value of the image digest identifiers = append(identifiers, strings.TrimPrefix(shaString, "sha256:")) } return identifiers } // subcomponentIdentifiersFromMatch returns the list of identifiers from the // package where grype did the match. func subcomponentIdentifiersFromMatch(m *match.Match) []string { ret := []string{} if m.Package.PURL != "" { ret = append(ret, m.Package.PURL) } // TODO(puerco):Implement CPE matching in openvex/go-vex /* for _, c := range m.Package.CPEs { ret = append(ret, c.String()) } */ return ret } // findMatchingStatement searches a VEX document for a statement matching the // given vulnerability. It performs a two-pass search: // 1. Try SBOM/context product identifiers (handles image-as-product cases) // 2. Try the match's own package identifiers as the product (handles // package-as-product cases, where the VEX product is a package PURL) func findMatchingStatement(doc *openvex.VEX, vulnID string, products []string, subcmp []string) (stmt *openvex.Statement, product string, subcomponents []string) { for _, product := range products { if stmts := doc.Matches(vulnID, product, subcmp); len(stmts) != 0 { return &stmts[0], product, subcmp } } for _, pkgID := range subcmp { if stmts := doc.Matches(vulnID, pkgID, nil); len(stmts) != 0 { return &stmts[0], pkgID, nil } } return nil, "", nil } // FilterMatches takes a set of scanning results and moves any results marked in // the VEX data as fixed or not_affected to the ignored list. func (ovm *Processor) FilterMatches( docRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch, ) (*match.Matches, []match.IgnoredMatch, error) { doc, ok := docRaw.(*openvex.VEX) if !ok { return nil, nil, errors.New("unable to cast vex document as openvex") } remainingMatches := match.NewMatches() products := productIdentifiersFromContext(pkgContext) // TODO(alex): should we apply the vex ignore rules to the already ignored matches? // that way the end user sees all of the reasons a match was ignored in case multiple apply // Now, let's go through grype's matches sorted := matches.Sorted() for i := range sorted { subcmp := subcomponentIdentifiersFromMatch(&sorted[i]) statement, _, _ := findMatchingStatement(doc, sorted[i].Vulnerability.ID, products, subcmp) // No data about this match's component. Next. if statement == nil { remainingMatches.Add(sorted[i]) continue } rule := matchingRule(ignoreRules, sorted[i], statement, vexStatus.IgnoreList()) if rule == nil { remainingMatches.Add(sorted[i]) continue } // Filtering only applies to not_affected and fixed statuses if statement.Status != openvex.StatusNotAffected && statement.Status != openvex.StatusFixed { remainingMatches.Add(sorted[i]) continue } ignoredMatches = append(ignoredMatches, match.IgnoredMatch{ Match: sorted[i], AppliedIgnoreRules: []match.IgnoreRule{*rule}, }) } return &remainingMatches, ignoredMatches, nil } // matchingRule cycles through a set of ignore rules and returns the first // one that matches the statement and the match. Returns nil if none match. func matchingRule(ignoreRules []match.IgnoreRule, m match.Match, statement *openvex.Statement, allowedStatuses []vexStatus.Status) *match.IgnoreRule { ms := match.NewMatches() ms.Add(m) // By default, if there are no ignore rules (which means the user didn't provide // any custom VEX rule), a matching rule should be returned if the statement // status is one of the allowed statuses. if len(ignoreRules) == 0 && slices.Contains(allowedStatuses, vexStatus.Status(statement.Status)) { return &match.IgnoreRule{ Namespace: "vex", Vulnerability: statement.Vulnerability.ID, VexJustification: string(statement.Justification), VexStatus: string(statement.Status), } } for _, rule := range ignoreRules { // If the rule has more conditions than just the VEX statement, check if // it applies to the current match. if rule.HasConditions() { r := rule r.VexStatus = "" if _, ignored := match.ApplyIgnoreRules(ms, []match.IgnoreRule{r}); len(ignored) == 0 { continue } } // If the status in the statement is not the same in the rule // and the vex statement, it does not apply if string(statement.Status) != rule.VexStatus { continue } // If the rule has a statement other than the allowed ones, skip: if rule.VexStatus != "" && !slices.Contains(allowedStatuses, vexStatus.Status(rule.VexStatus)) { continue } // If the rule applies to a VEX justification it needs to match the // statement, note that justifications only apply to not_affected: if statement.Status == openvex.StatusNotAffected && rule.VexJustification != "" && rule.VexJustification != string(statement.Justification) { continue } // If the vulnerability is blank in the rule it means we will honor // any status with any vulnerability. if rule.Vulnerability == "" { return &rule } // If the vulnerability is set, the rule applies if it is the same // in the statement and the rule. if statement.Vulnerability.Matches(rule.Vulnerability) { return &rule } } return nil } // AugmentMatches adds results to the match.Matches array when matching data // about an affected VEX product is found on loaded VEX documents. Matches // are moved from the ignore list or synthesized when no previous data is found. func (ovm *Processor) AugmentMatches( docRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, ) (*match.Matches, []match.IgnoredMatch, error) { doc, ok := docRaw.(*openvex.VEX) if !ok { return nil, nil, errors.New("unable to cast vex document as openvex") } additionalIgnoredMatches := []match.IgnoredMatch{} products := productIdentifiersFromContext(pkgContext) // Now, let's go through grype's matches for i := range ignoredMatches { subcmp := subcomponentIdentifiersFromMatch(&ignoredMatches[i].Match) statement, matchedProduct, matchedSubcmp := findMatchingStatement(doc, ignoredMatches[i].Vulnerability.ID, products, subcmp) // Only augment for affected or under_investigation statuses if statement == nil || (statement.Status != openvex.StatusAffected && statement.Status != openvex.StatusUnderInvestigation) { additionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i]) continue } // Only match if rules to augment are configured rule := matchingRule(ignoreRules, ignoredMatches[i].Match, statement, vexStatus.AugmentList()) if rule == nil { additionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i]) continue } newMatch := ignoredMatches[i].Match newMatch.Details = append(newMatch.Details, match.Detail{ Type: match.ExactDirectMatch, SearchedBy: &SearchedBy{ Vulnerability: ignoredMatches[i].Vulnerability.ID, Product: matchedProduct, Subcomponents: matchedSubcmp, }, Found: Match{ Statement: *statement, }, Matcher: match.OpenVexMatcher, }) remainingMatches.Add(newMatch) } return remainingMatches, additionalIgnoredMatches, nil } ================================================ FILE: grype/vex/openvex/implementation_test.go ================================================ package openvex import ( "strings" "testing" openvex "github.com/openvex/go-vex/pkg/vex" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/source" ) func TestIdentifiersFromTags(t *testing.T) { for _, tc := range []struct { sut string name string expected []string }{ { "alpine:v1.2.3", "alpine", []string{"alpine:v1.2.3", "pkg:oci/alpine?tag=v1.2.3"}, }, { "alpine", "alpine", []string{"alpine"}, }, } { res := identifiersFromTags([]string{tc.sut}, tc.name) require.Equal(t, tc.expected, res) } } func TestIdentifiersFromDigests(t *testing.T) { for _, tc := range []struct { sut string expected []string }{ { "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", []string{ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io%2Flibrary", "124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, { "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", []string{ "cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", "pkg:oci/curl@sha256%3A9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc?repository_url=cgr.dev%2Fchainguard", "9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc", }, }, { "alpine", []string{"alpine"}, }, } { res := identifiersFromDigests([]string{tc.sut}) require.Equal(t, tc.expected, res) } } func TestFilterMatches_NoErrorOnEmptyProducts(t *testing.T) { tests := []struct { name string pkgContext *pkg.Context vexDoc *openvex.VEX matches *match.Matches ignoreRules []match.IgnoreRule wantErr require.ErrorAssertionFunc }{ { name: "no error when context has empty products and VEX document has products", // when context returns empty products, the code should fall back to VEX products without error pkgContext: &pkg.Context{ Source: &source.Description{ Name: "alpine", Metadata: source.ImageMetadata{ Tags: []string{}, RepoDigests: []string{}, }, }, }, vexDoc: &openvex.VEX{ Statements: []openvex.Statement{ { Vulnerability: openvex.Vulnerability{Name: "CVE-2024-1234"}, Products: []openvex.Product{ {Component: openvex.Component{ID: "pkg:oci/alpine@sha256:abc123"}}, }, Status: openvex.StatusNotAffected, }, }, }, matches: func() *match.Matches { m := match.NewMatches() m.Add(match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2024-1234", }, }, Package: pkg.Package{ PURL: "pkg:npm/test@1.0.0", }, }) return &m }(), ignoreRules: []match.IgnoreRule{ {VexStatus: string(openvex.StatusNotAffected)}, }, }, { name: "no error when VEX document has multiple products", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "ubuntu", Metadata: source.ImageMetadata{ Tags: []string{}, RepoDigests: []string{}, }, }, }, vexDoc: &openvex.VEX{ Statements: []openvex.Statement{ { Vulnerability: openvex.Vulnerability{Name: "CVE-2024-5678"}, Products: []openvex.Product{ {Component: openvex.Component{ID: "pkg:oci/ubuntu@sha256:def456"}}, {Component: openvex.Component{ID: "pkg:oci/debian@sha256:abc789"}}, }, Status: openvex.StatusFixed, }, }, }, matches: func() *match.Matches { m := match.NewMatches() return &m }(), ignoreRules: []match.IgnoreRule{ {VexStatus: string(openvex.StatusFixed)}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } processor := New() remainingMatches, _, err := processor.FilterMatches( tt.vexDoc, tt.ignoreRules, tt.pkgContext, tt.matches, nil, ) tt.wantErr(t, err) if err != nil { return } // basic sanity checks - we're mainly testing that the fallback doesn't cause errors require.NotNil(t, remainingMatches) }) } } func TestFilterMatches_ImageProductNoSubcomponents(t *testing.T) { // Scenario 1: Image product, no subcomponents → applies to entire scan. // When a VEX statement specifies an image product with no subcomponents, // ALL matches for that products CVE should be filtered, regardless of which package. processor := New() pkgCtx := &pkg.Context{ Source: &source.Description{ Name: "alpine", Metadata: source.ImageMetadata{ RepoDigests: []string{ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, }, } vexDoc := &openvex.VEX{ Statements: []openvex.Statement{ { Vulnerability: openvex.Vulnerability{Name: "CVE-2023-1255"}, Products: []openvex.Product{ { Component: openvex.Component{ ID: "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, // No subcomponents — applies to entire product }, }, Status: openvex.StatusFixed, }, }, } matchLibcrypto := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1255", }, }, Package: pkg.Package{ ID: "cc8f90662d91481d", Name: "libcrypto3", PURL: "pkg:apk/alpine/libcrypto3@3.0.8-r3", }, } matchLibssl := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1255", }, }, Package: pkg.Package{ ID: "aa1234567890abcd", Name: "libssl3", PURL: "pkg:apk/alpine/libssl3@3.0.8-r3", }, } matches := match.NewMatches(matchLibcrypto, matchLibssl) remaining, ignored, err := processor.FilterMatches( vexDoc, nil, pkgCtx, &matches, nil, ) require.NoError(t, err) // Both matches should be filtered because there are no subcomponents require.Empty(t, remaining.Sorted(), "all matches for the CVE should be filtered when no subcomponents are specified") require.Len(t, ignored, 2, "both matches should be in the ignored list") } func TestFilterMatches_PackageProductDirectoryScan(t *testing.T) { // When the source is a directory scan and the VEX product is a package PURL, // the second pass of findMatchingStatement matches the package PURL as the product. processor := New() pkgCtx := &pkg.Context{ Source: &source.Description{ Metadata: source.DirectoryMetadata{ Path: "/some/project", }, }, } vexDoc := &openvex.VEX{ Statements: []openvex.Statement{ { Vulnerability: openvex.Vulnerability{Name: "CVE-2023-1255"}, Products: []openvex.Product{ { Component: openvex.Component{ ID: "pkg:apk/alpine/libcrypto3@3.0.8-r3", }, }, }, Status: openvex.StatusFixed, }, }, } matchLibcrypto := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1255", }, }, Package: pkg.Package{ ID: "cc8f90662d91481d", Name: "libcrypto3", PURL: "pkg:apk/alpine/libcrypto3@3.0.8-r3", }, } matches := match.NewMatches(matchLibcrypto) remaining, ignored, err := processor.FilterMatches( vexDoc, nil, pkgCtx, &matches, nil, ) require.NoError(t, err) require.Empty(t, remaining.Sorted(), "match should be filtered when package PURL matches VEX product") require.Len(t, ignored, 1, "match should be in the ignored list") } func TestFilterMatches_PackageProductNoOverMatch(t *testing.T) { // When the VEX product is a package PURL (not an image), only the matching // package should be filtered — not other packages with the same CVE. vexDoc := &openvex.VEX{ Statements: []openvex.Statement{ { Vulnerability: openvex.Vulnerability{Name: "CVE-2023-1255"}, Products: []openvex.Product{ { Component: openvex.Component{ ID: "pkg:apk/alpine/libcrypto3@3.0.8-r3", }, }, }, Status: openvex.StatusFixed, }, }, } matchLibcrypto := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1255", }, }, Package: pkg.Package{ ID: "cc8f90662d91481d", Name: "libcrypto3", PURL: "pkg:apk/alpine/libcrypto3@3.0.8-r3", }, } matchCurl := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1255", }, }, Package: pkg.Package{ ID: "bb9876543210fedc", Name: "curl", PURL: "pkg:apk/alpine/curl@8.1.2-r0", }, } tests := []struct { name string pkgContext *pkg.Context }{ { name: "image scan", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "alpine", Metadata: source.ImageMetadata{ RepoDigests: []string{ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, }, }, }, { name: "directory scan", pkgContext: &pkg.Context{ Source: &source.Description{ Metadata: source.DirectoryMetadata{ Path: "/some/project", }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { processor := New() matches := match.NewMatches(matchLibcrypto, matchCurl) remaining, ignored, err := processor.FilterMatches( vexDoc, nil, tt.pkgContext, &matches, nil, ) require.NoError(t, err) require.Len(t, remaining.Sorted(), 1, "only the non-matching package should remain") require.Equal(t, "curl", remaining.Sorted()[0].Package.Name) require.Len(t, ignored, 1, "only the matching package should be ignored") require.Equal(t, "libcrypto3", ignored[0].Match.Package.Name) }) } } func TestProductIdentifiersFromContext(t *testing.T) { tests := []struct { name string pkgContext *pkg.Context want []string }{ { name: "image metadata with tags and digests", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "alpine", Metadata: source.ImageMetadata{ Tags: []string{"alpine:3.18", "alpine:latest"}, RepoDigests: []string{ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, }, }, want: []string{ "alpine:3.18", "pkg:oci/alpine?tag=3.18", "alpine:latest", "pkg:oci/alpine?tag=latest", "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io%2Flibrary", "124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, { name: "image metadata with only tags", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "ubuntu", Metadata: source.ImageMetadata{ Tags: []string{"ubuntu:22.04"}, RepoDigests: []string{}, }, }, }, want: []string{ "ubuntu:22.04", "pkg:oci/ubuntu?tag=22.04", }, }, { name: "image metadata with only digests", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "nginx", Metadata: source.ImageMetadata{ Tags: []string{}, RepoDigests: []string{ "nginx@sha256:abc123", }, }, }, }, want: []string{ "nginx@sha256:abc123", }, }, { name: "image metadata with no tags or digests", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "busybox", Metadata: source.ImageMetadata{ Tags: []string{}, RepoDigests: []string{}, }, }, }, want: nil, }, { name: "generic source with name and version", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "MyApp", Version: "1.2.3", Metadata: source.DirectoryMetadata{ Path: "/some/path", }, }, }, want: []string{"pkg:generic/myapp@1.2.3"}, }, { name: "generic source with lowercase name", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "my-service", Version: "2.0.0", Metadata: source.FileMetadata{ Path: "/path/to/file", }, }, }, want: []string{"pkg:generic/my-service@2.0.0"}, }, { name: "generic source with only name", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "MyApp", Version: "", Metadata: source.DirectoryMetadata{ Path: "/some/path", }, }, }, want: []string{}, }, { name: "generic source with only version", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "", Version: "1.0.0", Metadata: source.DirectoryMetadata{ Path: "/some/path", }, }, }, want: []string{}, }, { name: "generic source with neither name nor version", pkgContext: &pkg.Context{ Source: &source.Description{ Name: "", Version: "", Metadata: source.DirectoryMetadata{ Path: "/some/path", }, }, }, want: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := productIdentifiersFromContext(tt.pkgContext) require.Equal(t, tt.want, got) }) } } func TestIdentifiersFromDigests_NormalizesDockerHubRepositoryURL(t *testing.T) { const hash = "124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126" const digest = "docker.io/library/alpine@sha256:" + hash ids := identifiersFromDigests([]string{digest}) var repoURL string for _, id := range ids { if !strings.HasPrefix(id, "pkg:oci/") { continue } p, err := packageurl.FromString(id) require.NoError(t, err) if p.Name == "alpine" && p.Version == "sha256:"+hash { repoURL = p.Qualifiers.Map()["repository_url"] break } } require.NotEmpty(t, repoURL, "expected to find alpine purl in identifiers: %#v", ids) require.Equal(t, "index.docker.io/library", repoURL) } func TestNormalizeDockerHubRepositoryURL(t *testing.T) { tests := []struct { input string expected string }{ {"docker.io/library", "index.docker.io/library"}, {"index.docker.io/library", "index.docker.io/library"}, {"registry-1.docker.io/library", "index.docker.io/library"}, {"https://docker.io/library", "index.docker.io/library"}, {"http://docker.io/library", "index.docker.io/library"}, {"gcr.io/myorg", "gcr.io/myorg"}, {"", ""}, {"DOCKER.IO/Library", "index.docker.io/Library"}, {"docker.io", "index.docker.io"}, {"docker.io/", "index.docker.io"}, {" docker.io/library ", "index.docker.io/library"}, } for _, tc := range tests { t.Run(tc.input, func(t *testing.T) { got := normalizeDockerHubRepositoryURL(tc.input) require.Equal(t, tc.expected, got) }) } } ================================================ FILE: grype/vex/processor.go ================================================ package vex import ( "fmt" "os" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vex/csaf" "github.com/anchore/grype/grype/vex/openvex" ) type Processor struct { Options ProcessorOptions impl vexProcessorImplementation } type vexProcessorImplementation interface { // ReadVexDocuments takes a list of vex filenames and returns a single // value representing the VEX information in the underlying implementation's // format. Returns an error if the files cannot be processed. ReadVexDocuments(docs []string) (interface{}, error) // FilterMatches matches receives the underlying VEX implementation VEX data and // the scanning context and matching results and filters the fixed and // not_affected results,moving them to the list of ignored matches. FilterMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) // AugmentMatches reads known affected VEX products from loaded documents and // adds new results to the scanner results when the product is marked as // affected in the VEX data. AugmentMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) } // getVexImplementation this function returns the vex processor implementation // at some point it can read the options and choose a user configured implementation. func getVexImplementation(documents []string) (vexProcessorImplementation, error) { // No documents, no implementation if len(documents) == 0 { return nil, nil } firstDoc := documents[0] if _, err := os.Stat(firstDoc); err != nil { return nil, fmt.Errorf("VEX document %q not found", firstDoc) } if csaf.IsCSAF(firstDoc) { return csaf.New(), nil } if openvex.IsOpenVex(firstDoc) { return openvex.New(), nil } return nil, fmt.Errorf("unsupported VEX document format for %q", firstDoc) } // NewProcessor returns a new VEX processor func NewProcessor(opts ProcessorOptions) (*Processor, error) { implementation, err := getVexImplementation(opts.Documents) if err != nil { return nil, fmt.Errorf("unable to create VEX processor: %w", err) } return &Processor{ Options: opts, impl: implementation, }, nil } // ProcessorOptions captures the options of the VEX processor. type ProcessorOptions struct { Documents []string IgnoreRules []match.IgnoreRule } // ApplyVEX receives the results from a scan run and applies any VEX information // in the files specified in the grype invocation. Any filtered results will // be moved to the ignored matches slice. func (vm *Processor) ApplyVEX(pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) { var err error // If no VEX documents are loaded, just pass through the matches, effectively NOOP if len(vm.Options.Documents) == 0 { return remainingMatches, ignoredMatches, nil } // Read VEX data from all passed documents rawVexData, err := vm.impl.ReadVexDocuments(vm.Options.Documents) if err != nil { return nil, nil, fmt.Errorf("parsing vex document: %w", err) } vexRules := extractVexRules(vm.Options.IgnoreRules) remainingMatches, ignoredMatches, err = vm.impl.FilterMatches( rawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches, ) if err != nil { return nil, nil, fmt.Errorf("checking matches against VEX data: %w", err) } remainingMatches, ignoredMatches, err = vm.impl.AugmentMatches( rawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches, ) if err != nil { return nil, nil, fmt.Errorf("checking matches to augment from VEX data: %w", err) } return remainingMatches, ignoredMatches, nil } // extractVexRules is a utility function that takes a set of ignore rules and // extracts those that act on VEX statuses. func extractVexRules(rules []match.IgnoreRule) []match.IgnoreRule { newRules := []match.IgnoreRule{} for _, r := range rules { if r.VexStatus != "" { newRules = append(newRules, r) newRules[len(newRules)-1].Namespace = "vex" } } return newRules } ================================================ FILE: grype/vex/processor_test.go ================================================ package vex import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vex/status" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/source" ) func TestProcessor_ApplyVEX(t *testing.T) { pkgContext := &pkg.Context{ Source: &source.Description{ Name: "alpine", Version: "3.17", Metadata: source.ImageMetadata{ RepoDigests: []string{ "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, }, } libCryptoPackage := pkg.Package{ ID: "cc8f90662d91481d", Name: "libcrypto3", Version: "3.0.8-r3", Type: "apk", PURL: "pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3", Upstreams: []pkg.UpstreamPackage{ { Name: "openssl", }, }, } libCryptoCVE_2023_3817 := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-3817", Namespace: "alpine:distro:alpine:3.17", }, Fix: vulnerability.Fix{ Versions: []string{"3.0.10-r0"}, State: vulnerability.FixStateFixed, }, }, Package: libCryptoPackage, } libCryptoCVE_2023_1255 := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-1255", Namespace: "alpine:distro:alpine:3.17", }, Fix: vulnerability.Fix{ Versions: []string{"3.0.8-r4"}, State: vulnerability.FixStateFixed, }, }, Package: libCryptoPackage, } libCryptoCVE_2023_2975 := match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2023-2975", Namespace: "alpine:distro:alpine:3.17", }, Fix: vulnerability.Fix{ Versions: []string{"3.0.9-r2"}, State: vulnerability.FixStateFixed, }, }, Package: libCryptoPackage, } getSubject := func() *match.Matches { s := match.NewMatches( // not-affected justification example libCryptoCVE_2023_3817, // fixed status example + matching CVE libCryptoCVE_2023_1255, // fixed status example libCryptoCVE_2023_2975, ) return &s } matchesRef := func(ms ...match.Match) *match.Matches { m := match.NewMatches(ms...) return &m } type args struct { pkgContext *pkg.Context matches *match.Matches ignoredMatches []match.IgnoredMatch } tests := []struct { name string options ProcessorOptions args args wantMatches *match.Matches wantIgnoredMatches []match.IgnoredMatch wantErr require.ErrorAssertionFunc }{ { name: "csaf-demo1 - ignore by fixed status", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/csaf-demo1.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: string(status.Fixed), }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_1255, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", VexStatus: string(status.Fixed), }}, }}, }, { name: "csaf-demo1 - ignore by fixed status and CVE", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/csaf-demo1.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: string(status.Fixed), Vulnerability: "CVE-2023-1255", // note: and previous tests }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_1255, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", Vulnerability: "CVE-2023-1255", VexStatus: string(status.Fixed), }}, }}, }, { name: "csaf-demo2 - ignore by not_affected status and vulnerable_code_not_present justification", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/csaf-demo2.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: string(status.NotAffected), VexJustification: "vulnerable_code_not_present", // note: this is the difference between this test and previous tests }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_1255, libCryptoCVE_2023_2975), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_3817, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", VexJustification: "vulnerable_code_not_present", VexStatus: string(status.NotAffected), }}, }}, }, { name: "openvex-demo1 - ignore by fixed status", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/openvex-demo1.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: string(status.Fixed), }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_1255, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", VexStatus: string(status.Fixed), }}, }}, }, { name: "openvex-demo1 - ignore by fixed status and CVE", // no real difference from the first test other than the AppliedIgnoreRules options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/openvex-demo1.json", }, IgnoreRules: []match.IgnoreRule{{ Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test VexStatus: string(status.Fixed), }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_1255, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test VexStatus: string(status.Fixed), }}, }}, }, { name: "openvex-demo2 - ignore by fixed status", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/openvex-demo2.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: string(status.Fixed), }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_3817), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_1255, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", VexStatus: string(status.Fixed), }}, }, { Match: libCryptoCVE_2023_2975, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", VexStatus: string(status.Fixed), }}, }}, }, { name: "openvex-demo2 - ignore by fixed status and CVE", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/openvex-demo2.json", }, IgnoreRules: []match.IgnoreRule{{ Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test VexStatus: string(status.Fixed), }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_1255, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", Vulnerability: "CVE-2023-1255", // note: this is the difference between this test and the last test VexStatus: string(status.Fixed), }}, }}, }, { name: "openvex-demo1 - ignore by not_affected status and vulnerable_code_not_present justification", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/openvex-demo1.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: "not_affected", VexJustification: "vulnerable_code_not_present", }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, // nothing gets ignored! wantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975, libCryptoCVE_2023_1255), wantIgnoredMatches: []match.IgnoredMatch{}, }, { name: "openvex-demo2 - ignore by not_affected status and vulnerable_code_not_present justification", options: ProcessorOptions{ Documents: []string{ "testdata/vex-docs/openvex-demo2.json", }, IgnoreRules: []match.IgnoreRule{{ VexStatus: "not_affected", VexJustification: "vulnerable_code_not_present", }}, }, args: args{ pkgContext: pkgContext, matches: getSubject(), }, wantMatches: matchesRef(libCryptoCVE_2023_2975, libCryptoCVE_2023_1255), wantIgnoredMatches: []match.IgnoredMatch{{ Match: libCryptoCVE_2023_3817, AppliedIgnoreRules: []match.IgnoreRule{{ Namespace: "vex", VexStatus: "not_affected", VexJustification: "vulnerable_code_not_present", }}, }}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } p, err := NewProcessor(tt.options) tt.wantErr(t, err) if err != nil { return } actualMatches, actualIgnoredMatches, err := p.ApplyVEX(tt.args.pkgContext, tt.args.matches, tt.args.ignoredMatches) tt.wantErr(t, err) if err != nil { return } assert.Equal(t, tt.wantMatches.Sorted(), actualMatches.Sorted()) assert.Equal(t, tt.wantIgnoredMatches, actualIgnoredMatches) }) } } ================================================ FILE: grype/vex/status/status.go ================================================ package status type Status string // VEX statuses as defined by CISA // https://www.cisa.gov/sites/default/files/2023-04/minimum-requirements-for-vex-508c.pdf // // Different VEX implementation can use different names to refer to them const ( NotAffected Status = "not_affected" Affected Status = "affected" Fixed Status = "fixed" UnderInvestigation Status = "under_investigation" ) // AugmentList returns the VEX statuses that augment results func AugmentList() []Status { return []Status{Affected, UnderInvestigation} } // IgnoreList returns the VEX statuses that should be ignored func IgnoreList() []Status { return []Status{Fixed, NotAffected} } ================================================ FILE: grype/vex/testdata/vex-docs/csaf-demo1.json ================================================ { "document": { "category": "csaf_vex", "csaf_version": "2.0", "notes": [ { "category": "summary", "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", "title": "Author comment" } ], "publisher": { "category": "vendor", "name": "Example Company ProductCERT", "namespace": "https://psirt.example.com" }, "title": "AquaSecurity example VEX document", "tracking": { "current_release_date": "2022-03-03T11:00:00.000Z", "generator": { "date": "2022-03-03T11:00:00.000Z", "engine": { "name": "Secvisogram", "version": "1.11.0" } }, "id": "2022-EVD-UC-01-A-001", "initial_release_date": "2022-03-03T11:00:00.000Z", "revision_history": [ { "date": "2022-03-03T11:00:00.000Z", "number": "1", "summary": "Initial version." } ], "status": "final", "version": "1" } }, "product_tree": { "branches": [ { "branches": [ { "branches": [ { "category": "product_version", "name": "2.6.0", "product": { "name": "Spring Boot 2.6.0", "product_id": "SPB-00260", "product_identification_helper": { "purl": "pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3" } } } ], "category": "product_name", "name": "Spring Boot" } ], "category": "vendor", "name": "Spring" } ] }, "vulnerabilities": [ { "cve": "CVE-2023-1255", "product_status": { "fixed": [ "SPB-00260" ] }, "threats": [ { "category": "impact", "details": "Class with vulnerable code was removed before shipping.", "product_ids": [ "SPB-00260" ] } ] } ] } ================================================ FILE: grype/vex/testdata/vex-docs/csaf-demo2.json ================================================ { "document": { "category": "csaf_vex", "csaf_version": "2.0", "notes": [ { "category": "summary", "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", "title": "Author comment" } ], "publisher": { "category": "vendor", "name": "Example Company ProductCERT", "namespace": "https://psirt.example.com" }, "title": "AquaSecurity example VEX document", "tracking": { "current_release_date": "2022-03-03T11:00:00.000Z", "generator": { "date": "2022-03-03T11:00:00.000Z", "engine": { "name": "Secvisogram", "version": "1.11.0" } }, "id": "2022-EVD-UC-01-A-001", "initial_release_date": "2022-03-03T11:00:00.000Z", "revision_history": [ { "date": "2022-03-03T11:00:00.000Z", "number": "1", "summary": "Initial version." } ], "status": "final", "version": "1" } }, "product_tree": { "branches": [ { "branches": [ { "branches": [ { "category": "product_version", "name": "2.6.0", "product": { "name": "Spring Boot 2.6.0", "product_id": "SPB-00260", "product_identification_helper": { "purl": "pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3" } } } ], "category": "product_name", "name": "Spring Boot" } ], "category": "vendor", "name": "Spring" } ] }, "vulnerabilities": [ { "cve": "CVE-2023-3817", "flags": [ { "label": "vulnerable_code_not_present", "product_ids": [ "SPB-00260" ] } ], "product_status": { "known_not_affected": [ "SPB-00260" ] } } ] } ================================================ FILE: grype/vex/testdata/vex-docs/openvex-debian.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", "author": "The OpenVEX Project ", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2014-fake-1" }, "products": [ { "@id": "pkg:oci/debian@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io/library" } ], "status": "fixed" } ] } ================================================ FILE: grype/vex/testdata/vex-docs/openvex-demo1.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", "author": "The OpenVEX Project ", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2023-1255" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "subcomponents": [ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ] } ], "status": "fixed" } ] } ================================================ FILE: grype/vex/testdata/vex-docs/openvex-demo2.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", "author": "The OpenVEX Project ", "role": "Demo Writer", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2023-1255" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "subcomponents": [ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ] } ], "status": "fixed" }, { "vulnerability": { "name": "CVE-2023-2650" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "subcomponents": [ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ] } ], "status": "fixed" }, { "vulnerability": { "name": "CVE-2023-2975" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "subcomponents": [ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ] } ], "status": "fixed" }, { "vulnerability": { "name": "CVE-2023-3446" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "subcomponents": [ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ] } ], "status": "not_affected", "justification": "vulnerable_code_not_present", "impact_statement": "affected functions were removed before packaging" }, { "vulnerability": { "name": "CVE-2023-3817" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", "subcomponents": [ { "@id": "pkg:apk/alpine/libssl3@3.0.8-r3" }, { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ] } ], "status": "not_affected", "justification": "vulnerable_code_not_present", "impact_statement": "affected functions were removed before packaging" } ] } ================================================ FILE: grype/vex/testdata/vex-docs/openvex-image-no-subcomponents.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-image-no-subcomponents", "author": "The OpenVEX Project ", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2023-1255" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126" } ], "status": "fixed" } ] } ================================================ FILE: grype/vex/testdata/vex-docs/openvex-package-product.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-package-product", "author": "The OpenVEX Project ", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2023-1255" }, "products": [ { "@id": "pkg:apk/alpine/libcrypto3@3.0.8-r3" } ], "status": "fixed" } ] } ================================================ FILE: grype/vulnerability/advisory.go ================================================ package vulnerability type Advisory struct { ID string Link string } ================================================ FILE: grype/vulnerability/fix.go ================================================ package vulnerability import "time" type FixState string const ( FixStateUnknown FixState = "unknown" FixStateFixed FixState = "fixed" FixStateNotFixed FixState = "not-fixed" FixStateWontFix FixState = "wont-fix" ) func AllFixStates() []FixState { return []FixState{ FixStateFixed, FixStateNotFixed, FixStateUnknown, FixStateWontFix, } } type Fix struct { Versions []string State FixState Available []FixAvailable } type FixAvailable struct { Version string Date time.Time Kind string } func (f FixState) String() string { return string(f) } ================================================ FILE: grype/vulnerability/metadata.go ================================================ package vulnerability import ( "strings" "time" ) type Metadata struct { ID string DataSource string // the primary reference URL, i.e. where the data originated Namespace string Severity string URLs []string // secondary reference URLs a vulnerability may provide Description string Cvss []Cvss KnownExploited []KnownExploited EPSS []EPSS CWEs []CWE // calculated as-needed risk float64 } // RiskScore computes a basic quantitative risk by combining threat and severity. // Threat is represented by epss (likelihood of exploitation), and severity by the cvss base score + string severity. // Impact is currently fixed at 1 and may be integrated into the calculation in future versions. // Raw risk is epss * (cvss / 10) * impact, then scaled to 0–100 for readability. // If a vulnerability appears in the KEV list, apply an additional boost to reflect known exploitation. // Known ransomware campaigns receive a further, distinct boost. func (m *Metadata) RiskScore() float64 { if m == nil { return 0 } if m.risk != 0 { return m.risk } m.risk = riskScore(*m) return m.risk } func riskScore(m Metadata) float64 { return min(threat(m)*severity(m)*kevModifier(m), 1.0) * 100.0 } func kevModifier(m Metadata) float64 { if len(m.KnownExploited) > 0 { for _, kev := range m.KnownExploited { if strings.ToLower(kev.KnownRansomwareCampaignUse) == "known" { // consider ransomware campaigns to be a greater kevModifier than other KEV threats return 1.1 } } return 1.05 // boost the final result, as if there is a greater kevModifier inherently from KEV threats } return 1.0 } func threat(m Metadata) float64 { if len(m.KnownExploited) > 0 { // per the EPSS guidance, any evidence of exploitation in the wild (not just PoC) should be considered over EPSS data return 1.0 } if len(m.EPSS) == 0 { return 0.0 } return m.EPSS[0].EPSS } // severity returns a 0-1 value, which is a combination of the string severity and the average of the cvss base scores. // If there are no cvss scores, the string severity is used. Some vendors only update the string severity and not the // cvss scores, so it's important to consider all sources. We are also not biasing towards any one source (multiple // cvss scores won't over-weigh the string severity). func severity(m Metadata) float64 { // TODO: summarization should take a policy: prefer NVD over CNA or vice versa... stringSeverityScore := severityToScore(m.Severity) / 10.0 avgBaseScore := average(validBaseScores(m.Cvss...)...) / 10.0 if avgBaseScore == 0 { return stringSeverityScore } return average(stringSeverityScore, avgBaseScore) } func severityToScore(severity string) float64 { // use the middle of the range for each severity switch strings.ToLower(severity) { case "negligible": return 0.5 case "low": return 3.0 case "medium": return 5.0 case "high": return 7.5 case "critical": return 9.0 } // the severity value might be "unknown" or an unexpected value. These should not be lost // in the noise and placed at the bottom of the list... instead we compromise to the middle of the list. return 5.0 } func validBaseScores(as ...Cvss) []float64 { var out []float64 for _, a := range as { if a.Metrics.BaseScore == 0 { // this is a mistake... base scores cannot be 0. Don't include this value and bring down the average continue } out = append(out, a.Metrics.BaseScore) } return out } func average(as ...float64) float64 { if len(as) == 0 { return 0 } sum := 0.0 for _, a := range as { sum += a } return sum / float64(len(as)) } type Cvss struct { Source string Type string Version string Vector string Metrics CvssMetrics VendorMetadata interface{} } type CvssMetrics struct { BaseScore float64 ExploitabilityScore *float64 ImpactScore *float64 } type KnownExploited struct { CVE string VendorProject string Product string DateAdded *time.Time RequiredAction string DueDate *time.Time KnownRansomwareCampaignUse string Notes string URLs []string CWEs []string } type EPSS struct { CVE string EPSS float64 Percentile float64 Date time.Time } type CWE struct { CVE string CWE string Source string Type string } ================================================ FILE: grype/vulnerability/metadata_test.go ================================================ package vulnerability import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRiskScore(t *testing.T) { tests := []struct { name string metadata Metadata expected float64 }{ { name: "nil metadata", metadata: Metadata{}, expected: 0, }, { name: "already calculated risk", metadata: Metadata{ risk: 42.5, }, expected: 42.5, }, { name: "no EPSS data, no KEV", metadata: Metadata{ Severity: "high", Cvss: []Cvss{ { Metrics: CvssMetrics{ BaseScore: 7.5, }, }, }, }, expected: 0, // threat is 0 without EPSS or KEV }, { name: "with EPSS data, no KEV", metadata: Metadata{ Severity: "high", EPSS: []EPSS{ { EPSS: 0.5, Percentile: 0.95, }, }, Cvss: []Cvss{ { Metrics: CvssMetrics{ BaseScore: 7.5, }, }, }, }, expected: 37.5, // 0.5 * (7.5/10) * 1 * 100 }, { name: "with KEV, no EPSS", metadata: Metadata{ Severity: "high", KnownExploited: []KnownExploited{ { CVE: "CVE-2023-1234", KnownRansomwareCampaignUse: "No", }, }, Cvss: []Cvss{ { Metrics: CvssMetrics{ BaseScore: 7.5, }, }, }, }, expected: 78.75, // 1.0 * (7.5/10) * 1.05* 100 }, { name: "with KEV ransomware", metadata: Metadata{ Severity: "high", KnownExploited: []KnownExploited{ { CVE: "CVE-2023-1234", KnownRansomwareCampaignUse: "Known", }, }, Cvss: []Cvss{ { Metrics: CvssMetrics{ BaseScore: 7.5, }, }, }, }, expected: 82.5, // 1.0 * (7.5/10) * 1.1 * 100 }, { name: "with severity string only", metadata: Metadata{ Severity: "critical", EPSS: []EPSS{ { EPSS: 0.8, Percentile: 0.99, }, }, }, expected: 72, // 0.8 * (9.0/10) * 1.0 * 100 }, { name: "with multiple CVSS scores + string severity", metadata: Metadata{ Severity: "medium", EPSS: []EPSS{ { EPSS: 0.6, Percentile: 0.90, }, }, Cvss: []Cvss{ { Source: "NVD", Metrics: CvssMetrics{ BaseScore: 6.5, }, }, { Source: "Vendor", Metrics: CvssMetrics{ BaseScore: 5.5, }, }, }, }, expected: 33, // 0.6 * ( (((6.5+5.5)/2)+5)/2 /10) * 1.0 * 100 }, { name: "with some invalid CVSS scores + string severity", metadata: Metadata{ Severity: "medium", EPSS: []EPSS{ { EPSS: 0.4, Percentile: 0.85, }, }, Cvss: []Cvss{ { Source: "NVD", Metrics: CvssMetrics{ BaseScore: 0, // invalid, should be ignored }, }, { Source: "Vendor", Metrics: CvssMetrics{ BaseScore: 6.0, }, }, }, }, expected: 22, // 0.4 * ((6.0+5)/2 /10) * 1.0 * 100 }, { name: "unknown severity", metadata: Metadata{ Severity: "unknown", EPSS: []EPSS{ { EPSS: 0.3, Percentile: 0.80, }, }, }, expected: 15, // 0.3 * (5.0/10) * 1.0 * 100 }, { name: "maximum risk clamp", metadata: Metadata{ Severity: "critical", KnownExploited: []KnownExploited{ { CVE: "CVE-2023-1234", KnownRansomwareCampaignUse: "Known", }, }, Cvss: []Cvss{ { Metrics: CvssMetrics{ BaseScore: 10.0, }, }, }, }, expected: 100, // clamped to 100 as it would be 1.0 * 1.0 * 1.1 * 100 = 120 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.metadata.RiskScore() assert.InDelta(t, tt.expected, result, 0.01, "RiskScore method returned incorrect value") // test the calculated value is cached if tt.name != "already calculated risk" && tt.name != "nil metadata" { require.InDelta(t, tt.expected, tt.metadata.risk, 0.01, "risk was not cached") } // test the standalone function if tt.name != "nil metadata" && tt.name != "already calculated risk" { funcResult := riskScore(tt.metadata) assert.InDelta(t, tt.expected, funcResult, 0.0001, "riskScore function returned incorrect value") } }) } } func TestSeverityToScore(t *testing.T) { tests := []struct { severity string expected float64 }{ {"negligible", 0.5}, {"NEGLIGIBLE", 0.5}, {"low", 3.0}, {"LOW", 3.0}, {"medium", 5.0}, {"MEDIUM", 5.0}, {"high", 7.5}, {"HIGH", 7.5}, {"critical", 9.0}, {"CRITICAL", 9.0}, {"unknown", 5.0}, {"", 5.0}, {"something-else", 5.0}, } for _, tt := range tests { t.Run(tt.severity, func(t *testing.T) { result := severityToScore(tt.severity) assert.Equal(t, tt.expected, result) }) } } func TestAverageCVSS(t *testing.T) { tests := []struct { name string cvss []Cvss expected float64 }{ { name: "empty slice", cvss: []Cvss{}, expected: 0, }, { name: "single valid score", cvss: []Cvss{ {Metrics: CvssMetrics{BaseScore: 7.5}}, }, expected: 7.5, }, { name: "multiple valid scores", cvss: []Cvss{ {Metrics: CvssMetrics{BaseScore: 7.5}}, {Metrics: CvssMetrics{BaseScore: 8.5}}, {Metrics: CvssMetrics{BaseScore: 9.0}}, }, expected: 8.33333, }, { name: "with invalid scores", cvss: []Cvss{ {Metrics: CvssMetrics{BaseScore: 0}}, // invalid {Metrics: CvssMetrics{BaseScore: 7.5}}, {Metrics: CvssMetrics{BaseScore: 0}}, // invalid {Metrics: CvssMetrics{BaseScore: 8.5}}, }, expected: 8.0, }, { name: "all invalid scores", cvss: []Cvss{ {Metrics: CvssMetrics{BaseScore: 0}}, {Metrics: CvssMetrics{BaseScore: 0}}, }, expected: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := average(validBaseScores(tt.cvss...)...) assert.InDelta(t, tt.expected, result, 0.00001) }) } } func TestThreat(t *testing.T) { tests := []struct { name string metadata Metadata expected float64 }{ { name: "no EPSS, no KEV", metadata: Metadata{}, expected: 0, }, { name: "with EPSS, no KEV", metadata: Metadata{ EPSS: []EPSS{ {EPSS: 0.75}, }, }, expected: 0.75, }, { name: "with KEV, no EPSS", metadata: Metadata{ KnownExploited: []KnownExploited{ {CVE: "CVE-2023-1234"}, }, }, expected: 1.0, }, { name: "with KEV and EPSS", metadata: Metadata{ EPSS: []EPSS{ {EPSS: 0.5}, }, KnownExploited: []KnownExploited{ {CVE: "CVE-2023-1234"}, }, }, expected: 1.0, // KEV takes precedence }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := threat(tt.metadata) assert.Equal(t, tt.expected, result) }) } } func TestImpact(t *testing.T) { tests := []struct { name string metadata Metadata expected float64 }{ { name: "no KEV", metadata: Metadata{}, expected: 1.0, }, { name: "KEV without ransomware", metadata: Metadata{ KnownExploited: []KnownExploited{ {KnownRansomwareCampaignUse: "No"}, }, }, expected: 1.05, }, { name: "KEV with ransomware", metadata: Metadata{ KnownExploited: []KnownExploited{ {KnownRansomwareCampaignUse: "Known"}, }, }, expected: 1.1, }, { name: "KEV with case insensitive ransomware", metadata: Metadata{ KnownExploited: []KnownExploited{ {KnownRansomwareCampaignUse: "KNOWN"}, }, }, expected: 1.1, }, { name: "multiple KEV entries, one with ransomware", metadata: Metadata{ KnownExploited: []KnownExploited{ {KnownRansomwareCampaignUse: "No"}, {KnownRansomwareCampaignUse: "Known"}, }, }, expected: 1.1, // highest wins }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := kevModifier(tt.metadata) assert.Equal(t, tt.expected, result) }) } } func TestSeverity(t *testing.T) { tests := []struct { name string metadata Metadata expected float64 }{ { name: "no CVSS, medium severity", metadata: Metadata{ Severity: "medium", }, expected: 0.5, }, { name: "with CVSS + severity string", metadata: Metadata{ Severity: "medium", Cvss: []Cvss{ {Metrics: CvssMetrics{BaseScore: 8.0}}, }, }, expected: 0.65, }, { name: "multiple CVSS scores + severity string", metadata: Metadata{ Severity: "medium", Cvss: []Cvss{ {Metrics: CvssMetrics{BaseScore: 6.0}}, {Metrics: CvssMetrics{BaseScore: 8.0}}, }, }, expected: 0.6, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := severity(tt.metadata) assert.InDelta(t, tt.expected, result, 0.00001) }) } } ================================================ FILE: grype/vulnerability/mock/vulnerability_provider.go ================================================ package mock import ( "github.com/anchore/grype/grype/db/v6/name" grypePkg "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vulnerability" ) // VulnerabilityProvider returns a new mock implementation of a vulnerability Provider, with the provided set of vulnerabilities func VulnerabilityProvider(vulnerabilities ...vulnerability.Vulnerability) vulnerability.Provider { return &mockProvider{ Vulnerabilities: vulnerabilities, } } type mockProvider struct { Vulnerabilities []vulnerability.Vulnerability Unaffected bool } func (s *mockProvider) Close() error { return nil } func (s *mockProvider) PackageSearchNames(p grypePkg.Package) []string { return name.PackageNames(p) } // VulnerabilityMetadata returns the metadata associated with a vulnerability func (s *mockProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) { for _, vuln := range s.Vulnerabilities { if vuln.ID == ref.ID && vuln.Namespace == ref.Namespace { var meta *vulnerability.Metadata if m, ok := vuln.Internal.(vulnerability.Metadata); ok { meta = &m } if m, ok := vuln.Internal.(*vulnerability.Metadata); ok { meta = m } if meta != nil { if meta.ID != vuln.ID { meta.ID = vuln.ID } if meta.Namespace != vuln.Namespace { meta.Namespace = vuln.Namespace } return meta, nil } } } return nil, nil } func (s *mockProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) { if err := search.ValidateCriteria(criteria); err != nil { return nil, err } var out []vulnerability.Vulnerability out = append(out, s.Vulnerabilities...) return filterE(out, func(v vulnerability.Vulnerability) (bool, error) { for _, row := range search.CriteriaIterator(criteria) { for _, c := range row { matches, _, err := c.MatchesVulnerability(v) if !matches || err != nil { return false, err } } } return true, nil }) } func filterE[T any](out []T, keep func(v T) (bool, error)) ([]T, error) { for i := 0; i < len(out); i++ { ok, err := keep(out[i]) if err != nil { return nil, err } if !ok { out = append(out[:i], out[i+1:]...) i-- } } return out, nil } ================================================ FILE: grype/vulnerability/provider.go ================================================ package vulnerability import ( "encoding/json" "io" "time" "github.com/anchore/grype/grype/distro" grypePkg "github.com/anchore/grype/grype/pkg" ) // Criteria interfaces are used for FindVulnerabilities calls type Criteria interface { // MatchesVulnerability returns true if the provided value meets the criteria MatchesVulnerability(value Vulnerability) (bool, string, error) } // MetadataProvider implementations provide ways to look up vulnerability metadata // // Deprecated: vulnerability.Vulnerability objects now have metadata included type MetadataProvider interface { // VulnerabilityMetadata returns the metadata associated with a vulnerability // // Deprecated: vulnerability.Vulnerability objects now have metadata included VulnerabilityMetadata(ref Reference) (*Metadata, error) } // Provider is the common interface for vulnerability sources to provide searching and metadata, such as a database type Provider interface { PackageSearchNames(grypePkg.Package) []string // FindVulnerabilities returns vulnerabilities matching all the provided criteria FindVulnerabilities(criteria ...Criteria) ([]Vulnerability, error) MetadataProvider io.Closer } type StoreMetadataProvider interface { DataProvenance() (map[string]DataProvenance, error) } // EOLChecker is an optional interface that vulnerability providers can implement // to check the end-of-life status for an operating system. EOL lookups use exact // matching (no aliasing) since each distro has its own EOL dates. type EOLChecker interface { // GetOperatingSystemEOL returns the EOL date for the given distro. // Returns nil dates if no EOL data is available for this distro. // Uses exact matching (no aliasing) since distros like CentOS have // different EOL dates than their alias targets like RHEL. GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) } type DataProvenance struct { DateCaptured time.Time `json:"captured,omitempty"` InputDigest string `json:"input,omitempty"` } type ProviderStatus struct { SchemaVersion string `json:"schemaVersion"` From string `json:"from,omitempty"` Built time.Time `json:"built,omitempty"` Path string `json:"path,omitempty"` Error error `json:"error,omitempty"` } func (s ProviderStatus) MarshalJSON() ([]byte, error) { errStr := "" if s.Error != nil { errStr = s.Error.Error() } var t string if !s.Built.IsZero() { t = s.Built.Format(time.RFC3339) } return json.Marshal(&struct { SchemaVersion string `json:"schemaVersion"` From string `json:"from,omitempty"` Built string `json:"built,omitempty"` Path string `json:"path,omitempty"` Valid bool `json:"valid"` Error string `json:"error,omitempty"` }{ SchemaVersion: s.SchemaVersion, From: s.From, Built: t, Path: s.Path, Valid: s.Error == nil, Error: errStr, }) } func (s DataProvenance) MarshalJSON() ([]byte, error) { var t string if !s.DateCaptured.IsZero() { t = s.DateCaptured.Format(time.RFC3339) } return json.Marshal(&struct { DateCaptured string `json:"captured,omitempty"` InputDigest string `json:"input,omitempty"` }{ DateCaptured: t, InputDigest: s.InputDigest, }) } ================================================ FILE: grype/vulnerability/severity.go ================================================ package vulnerability import "strings" const ( UnknownSeverity Severity = iota NegligibleSeverity LowSeverity MediumSeverity HighSeverity CriticalSeverity ) var matcherTypeStr = []string{ "unknown", // "unknown severity", "negligible", "low", "medium", "high", "critical", } func AllSeverities() []Severity { return []Severity{ NegligibleSeverity, LowSeverity, MediumSeverity, HighSeverity, CriticalSeverity, } } type Severity int type Severities []Severity func (f Severity) String() string { if int(f) >= len(matcherTypeStr) || f < 0 { return matcherTypeStr[0] } return matcherTypeStr[f] } func ParseSeverity(severity string) Severity { switch strings.ToLower(severity) { case NegligibleSeverity.String(): return NegligibleSeverity case LowSeverity.String(): return LowSeverity case MediumSeverity.String(): return MediumSeverity case HighSeverity.String(): return HighSeverity case CriticalSeverity.String(): return CriticalSeverity default: return UnknownSeverity } } func (s Severities) Len() int { return len(s) } func (s Severities) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s Severities) Less(i, j int) bool { return s[i] < s[j] } ================================================ FILE: grype/vulnerability/vulnerability.go ================================================ package vulnerability import ( "fmt" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/cpe" ) type Reference struct { ID string Namespace string Internal any } type Vulnerability struct { Reference Status string PackageName string Constraint version.Constraint PackageQualifiers []qualifier.Qualifier CPEs []cpe.CPE Fix Fix Advisories []Advisory RelatedVulnerabilities []Reference Metadata *Metadata Unaffected bool } func (v Vulnerability) String() string { constraint := "(none)" if v.Constraint != nil { constraint = v.Constraint.String() } return fmt.Sprintf("Vuln(id=%s constraint=%q qualifiers=%+v)", v.ID, constraint, v.PackageQualifiers) } // LogDropped should be called with a properly resolved vulnerability ID in every location in-memory vulnerabilities' // are filtered out by any process. this can be the first stop to diagnosing why certain vulnerabilities do not show up // //go:noinline func LogDropped(id, op, dropReason string, context any) { log.WithFields("op", op, "reason", dropReason, "vulnerability", id, "context", context).Trace("dropped vuln") } ================================================ FILE: grype/vulnerability_matcher.go ================================================ package grype import ( "context" "errors" "fmt" "runtime/debug" "slices" "strings" "time" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" syftPkg "github.com/anchore/syft/syft/pkg" ) const ( branch = "├──" leaf = "└──" ) // AlertsConfig controls which alerts are tracked and reported during vulnerability matching. type AlertsConfig struct { // EnableEOLDistroWarnings enables tracking packages from end-of-life distros EnableEOLDistroWarnings bool } type VulnerabilityMatcher struct { VulnerabilityProvider vulnerability.Provider ExclusionProvider match.ExclusionProvider Matchers []match.Matcher IgnoreRules []match.IgnoreRule FailSeverity *vulnerability.Severity NormalizeByCVE bool VexProcessor *vex.Processor Alerts AlertsConfig // tracked packages with distro issues (populated during FindMatches) eolDistroPackages []pkg.Package distroDetectionFailed bool } func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Severity) *VulnerabilityMatcher { m.FailSeverity = severity return m } func (m *VulnerabilityMatcher) WithMatchers(matchers []match.Matcher) *VulnerabilityMatcher { m.Matchers = matchers return m } func (m *VulnerabilityMatcher) WithIgnoreRules(ignoreRules []match.IgnoreRule) *VulnerabilityMatcher { m.IgnoreRules = ignoreRules return m } // DistroDetectionFailed returns true if distro detection failed during scanning // (linux release info was present but distro type could not be determined). func (m *VulnerabilityMatcher) DistroDetectionFailed() bool { return m.distroDetectionFailed } // EOLDistroPackages returns packages from distros that have reached end-of-life. func (m *VulnerabilityMatcher) EOLDistroPackages() []pkg.Package { return m.eolDistroPackages } // FindMatches finds vulnerabilities for the given packages and package context. // FindMatches does not support context cancellation; for that, use // FindMatchesContext. func (m *VulnerabilityMatcher) FindMatches( pkgs []pkg.Package, pkgContext pkg.Context, ) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) { return m.FindMatchesContext(context.Background(), pkgs, pkgContext) } // FindMatchesContext finds vulnerabilities for the given packages and package // context, and supports context cancellation. func (m *VulnerabilityMatcher) FindMatchesContext( ctx context.Context, pkgs []pkg.Package, pkgContext pkg.Context, ) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) { progressMonitor := trackMatcher(len(pkgs)) // capture distro detection failure from pkgContext for alerting m.distroDetectionFailed = pkgContext.DistroDetectionFailed if m.distroDetectionFailed { log.Warn("distro detection failed: linux release info was present but distro type could not be determined") } defer func() { progressMonitor.Ignored.Set(int64(len(ignoredMatches))) progressMonitor.SetCompleted() if err != nil { progressMonitor.MatchesDiscovered.SetError(err) } }() remainingMatches, ignoredMatches, err = m.findDBMatches(ctx, pkgs, progressMonitor) if err != nil { err = fmt.Errorf("unable to find matches against vulnerability database: %w", err) return remainingMatches, ignoredMatches, err } remainingMatches, ignoredMatches, err = m.findVEXMatches(pkgContext, remainingMatches, ignoredMatches, progressMonitor) if err != nil { err = fmt.Errorf("unable to find matches against VEX sources: %w", err) return remainingMatches, ignoredMatches, err } if m.FailSeverity != nil && hasSeverityAtOrAbove(m.VulnerabilityProvider, *m.FailSeverity, *remainingMatches) { err = grypeerr.ErrAboveSeverityThreshold return remainingMatches, ignoredMatches, err } logListSummary(progressMonitor) logIgnoredMatches(ignoredMatches) return remainingMatches, ignoredMatches, nil } func (m *VulnerabilityMatcher) findDBMatches(ctx context.Context, pkgs []pkg.Package, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { var ignoredMatches []match.IgnoredMatch log.Trace("finding matches against DB") matches, err := m.searchDBForMatches(ctx, pkgs, progressMonitor) if err != nil { if match.IsFatalError(err) { return nil, nil, err } // other errors returned from matchers during searchDBForMatches were being // logged and not returned, so just log them here log.WithFields("error", err).Debug("error(s) returned from searchDBForMatches") } matches, ignoredMatches = m.applyIgnoreRules(matches) if m.NormalizeByCVE { normalizedMatches := match.NewMatches() for originalMatch := range matches.Enumerate() { normalizedMatches.Add(m.normalizeByCVE(originalMatch)) } // we apply the ignore rules again in case any of the transformations done during normalization // regresses the results (relative to the already applied ignore rules). Why do we additionally apply // the ignore rules before normalizing? In case the user has a rule that ignores a non-normalized // vulnerability ID, we wantMatches to ensure that the rule is honored. originalIgnoredMatches := ignoredMatches matches, ignoredMatches = m.applyIgnoreRules(normalizedMatches) ignoredMatches = m.mergeIgnoredMatches(originalIgnoredMatches, ignoredMatches) } return &matches, ignoredMatches, nil } func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match.IgnoredMatch) []match.IgnoredMatch { var out []match.IgnoredMatch for _, ignoredMatches := range allIgnoredMatches { for _, ignored := range ignoredMatches { if m.NormalizeByCVE { ignored.Match = m.normalizeByCVE(ignored.Match) } out = append(out, ignored) } } return out } //nolint:funlen func (m *VulnerabilityMatcher) searchDBForMatches( ctx context.Context, packages []pkg.Package, progressMonitor *monitorWriter, ) (match.Matches, error) { var allMatches []match.Match var allIgnorers []match.IgnoreFilter matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers) if defaultMatcher == nil { defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true}) } // reset tracked distro packages m.eolDistroPackages = nil // setup EOL tracking if enabled eolTracker := newEOLTracker(m.Alerts.EnableEOLDistroWarnings, m.VulnerabilityProvider) var matcherErrs []error for _, p := range packages { progressMonitor.PackagesProcessed.Increment() log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches") // track EOL distro packages if eolTracker.checkAndTrack(p) { m.eolDistroPackages = append(m.eolDistroPackages, p) } matchAgainst, ok := matcherIndex[p.Type] if !ok { matchAgainst = []match.Matcher{defaultMatcher} } for _, theMatcher := range matchAgainst { if err := ctx.Err(); err != nil { return match.Matches{}, err } matches, ignorers, err := callMatcherSafely(theMatcher, m.VulnerabilityProvider, p) if err != nil { if match.IsFatalError(err) { return match.Matches{}, err } log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher returned error") matcherErrs = append(matcherErrs, err) } allIgnorers = append(allIgnorers, ignorers...) // Filter out matches based on records in the database exclusion table and hard-coded rules filtered, dropped := match.ApplyExplicitIgnoreRules(m.ExclusionProvider, match.NewMatches(matches...)) additionalMatches := filtered.Sorted() logPackageMatches(p, additionalMatches) logExplicitDroppedPackageMatches(p, dropped) allMatches = append(allMatches, additionalMatches...) progressMonitor.MatchesDiscovered.Add(int64(len(additionalMatches))) // note: there is a difference between "ignore" and "dropped" matches. // ignored: matches that are filtered out due to user-provided ignore rules // dropped: matches that are filtered out due to hard-coded rules updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.VulnerabilityProvider) } } // apply ignores based on matchers returning ignore rules filtered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnorers)) logIgnoredMatches(dropped) // get deduplicated set of matches res := match.NewMatches(filtered...) // update the total discovered matches after removing all duplicates and ignores progressMonitor.MatchesDiscovered.Set(int64(res.Count())) return res, errors.Join(matcherErrs...) } func callMatcherSafely(m match.Matcher, vp vulnerability.Provider, p pkg.Package) (matches []match.Match, ignoredMatches []match.IgnoreFilter, err error) { // handle individual matcher panics defer func() { if e := recover(); e != nil { err = match.NewFatalError(m.Type(), fmt.Errorf("%v at:\n%s", e, string(debug.Stack()))) } }() return m.Match(vp, p) } func (m *VulnerabilityMatcher) findVEXMatches(pkgContext pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) { if m.VexProcessor == nil { log.Trace("no VEX documents provided, skipping VEX matching") return remainingMatches, ignoredMatches, nil } log.Trace("finding matches against available VEX documents") matchesAfterVex, ignoredMatchesAfterVex, err := m.VexProcessor.ApplyVEX(&pkgContext, remainingMatches, ignoredMatches) if err != nil { return nil, nil, fmt.Errorf("unable to find matches against VEX documents: %w", err) } diffMatches := matchesAfterVex.Diff(*remainingMatches) // note: this assumes that the diff can only be additive diffIgnoredMatches := ignoredMatchesDiff(ignoredMatchesAfterVex, ignoredMatches) updateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.VulnerabilityProvider) return matchesAfterVex, ignoredMatchesAfterVex, nil } // applyIgnoreRules applies the user-provided ignore rules, splitting ignored matches into a separate set func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) { var ignoredMatches []match.IgnoredMatch if len(m.IgnoreRules) == 0 { return matches, ignoredMatches } matches, ignoredMatches = match.ApplyIgnoreRules(matches, m.IgnoreRules) if count := len(ignoredMatches); count > 0 { log.Infof("ignoring %d matches due to user-provided ignore rules", count) } return matches, ignoredMatches } func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match { if isCVE(match.Vulnerability.ID) { return match } var effectiveCVERecordRefs []vulnerability.Reference for _, ref := range match.Vulnerability.RelatedVulnerabilities { if isCVE(ref.ID) { effectiveCVERecordRefs = append(effectiveCVERecordRefs, ref) break } } switch len(effectiveCVERecordRefs) { case 0: log.WithFields( "vuln", match.Vulnerability.ID, "package", displayPackage(match.Package), ).Trace("unable to find CVE record for vulnerability, skipping normalization") return match case 1: break default: log.WithFields( "refs", fmt.Sprintf("%+v", effectiveCVERecordRefs), "vuln", match.Vulnerability.ID, "package", displayPackage(match.Package), ).Trace("found multiple CVE records for vulnerability, skipping normalization") return match } ref := effectiveCVERecordRefs[0] upstreamMetadata, err := m.VulnerabilityProvider.VulnerabilityMetadata(ref) //nolint:staticcheck // deprecated API still used internally if err != nil { log.WithFields("id", ref.ID, "namespace", ref.Namespace, "error", err).Warn("unable to fetch effective CVE metadata") return match } if upstreamMetadata == nil { return match } originalRef := vulnerability.Reference{ ID: match.Vulnerability.ID, Namespace: match.Vulnerability.Namespace, } match.Vulnerability.ID = upstreamMetadata.ID match.Vulnerability.Namespace = upstreamMetadata.Namespace match.Vulnerability.RelatedVulnerabilities = []vulnerability.Reference{originalRef} return match } // ignoreRulesByIndex implements match.IgnoreFilter to filter each matching // package that overlaps by location and have the same vulnerability ID (CVE) type ignoreRulesByIndex struct { remainingFilters []match.IgnoreFilter locationIgnoreRules map[string][]match.IgnoreRule packageNameIgnoreRules map[string][]match.IgnoreRule } func (i ignoreRulesByIndex) IgnoreMatch(m match.Match) []match.IgnoreRule { if nameRules := i.packageNameIgnoreRules[m.Package.Name]; nameRules != nil { for _, rule := range nameRules { if matched := rule.IgnoreMatch(m); matched != nil { return matched } } } for _, l := range m.Package.Locations.ToSlice() { for _, rule := range i.locationIgnoreRules[l.RealPath] { if matched := rule.IgnoreMatch(m); matched != nil { return matched } } } for _, f := range i.remainingFilters { if matched := f.IgnoreMatch(m); matched != nil { return matched } } return nil } // ignoredMatchFilter creates an ignore filter based on location-based IgnoredMatches to filter out "the same" // vulnerabilities reported by other matchers based on overlapping file locations func ignoredMatchFilter(ignores []match.IgnoreFilter) match.IgnoreFilter { out := ignoreRulesByIndex{ locationIgnoreRules: map[string][]match.IgnoreRule{}, packageNameIgnoreRules: map[string][]match.IgnoreRule{}, } // the returned slice of remaining rules are not location-based rules out.remainingFilters = slices.DeleteFunc(ignores, func(ignore match.IgnoreFilter) bool { if rule, ok := ignore.(match.IgnoreRule); ok { // return true to remove rules handled with index lookups from the remaining filter list if rule.Package.Location != "" && !strings.ContainsRune(rule.Package.Location, '*') { out.locationIgnoreRules[rule.Package.Location] = append(out.locationIgnoreRules[rule.Package.Location], rule) return true } if rule.Package.Name != "" { // this rule is handled with location lookups, remove it from the remaining filter list out.packageNameIgnoreRules[rule.Package.Name] = append(out.packageNameIgnoreRules[rule.Package.Name], rule) return true } } return false }) return out } func displayPackage(p pkg.Package) string { if p.PURL != "" { return p.PURL } ty := p.Type if p.Type == "" { ty = "unknown" } return fmt.Sprintf("%s@%s (type=%s)", p.Name, p.Version, ty) } func ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch) []match.IgnoredMatch { // TODO(alex): the downside with this implementation is that it does not account for the same ignored match being // ignored for different reasons (the appliedIgnoreRules field). otherMap := make(map[match.Fingerprint]struct{}) for _, a := range other { otherMap[a.Fingerprint()] = struct{}{} } var diff []match.IgnoredMatch for _, b := range subject { if _, ok := otherMap[b.Fingerprint()]; !ok { diff = append(diff, b) } } return diff } func newMatcherIndex(matchers []match.Matcher) (map[syftPkg.Type][]match.Matcher, match.Matcher) { matcherIndex := make(map[syftPkg.Type][]match.Matcher) var defaultMatcher match.Matcher for _, m := range matchers { if m.Type() == match.StockMatcher { defaultMatcher = m continue } for _, t := range m.PackageTypes() { if _, ok := matcherIndex[t]; !ok { matcherIndex[t] = make([]match.Matcher, 0) } matcherIndex[t] = append(matcherIndex[t], m) log.Tracef("adding matcher: %+v", t) } } return matcherIndex, defaultMatcher } func isCVE(id string) bool { return strings.HasPrefix(strings.ToLower(id), "cve-") } //nolint:staticcheck // MetadataProvider is deprecated but still used internally func hasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool { if severity == vulnerability.UnknownSeverity { return false } for m := range matches.Enumerate() { metadata, err := store.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally if err != nil { continue } if vulnerability.ParseSeverity(metadata.Severity) >= severity { return true } } return false } func logListSummary(vl *monitorWriter) { log.Infof("found %d vulnerability matches across %d packages", vl.MatchesDiscovered.Current(), vl.PackagesProcessed.Current()) log.Debugf(" ├── fixed: %d", vl.Fixed.Current()) log.Debugf(" ├── ignored: %d (due to user-provided rule)", vl.Ignored.Current()) log.Debugf(" ├── dropped: %d (due to hard-coded correction)", vl.Dropped.Current()) log.Debugf(" └── matched: %d", vl.MatchesDiscovered.Current()) var unknownCount int64 if count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok { unknownCount = count.Current() } log.Debugf(" ├── %s: %d", vulnerability.UnknownSeverity.String(), unknownCount) allSeverities := vulnerability.AllSeverities() for idx, sev := range allSeverities { arm := selectArm(idx, len(allSeverities)) log.Debugf(" %s %s: %d", arm, sev.String(), vl.BySeverity[sev].Current()) } } //nolint:staticcheck // MetadataProvider is deprecated but still used internally func updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider) { for _, m := range matches { metadata, err := metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally if err != nil || metadata == nil { mon.BySeverity[vulnerability.UnknownSeverity].Increment() continue } sevManualProgress, ok := mon.BySeverity[vulnerability.ParseSeverity(metadata.Severity)] if !ok { mon.BySeverity[vulnerability.UnknownSeverity].Increment() continue } sevManualProgress.Increment() if m.Vulnerability.Fix.State == vulnerability.FixStateFixed { mon.Fixed.Increment() } } mon.Ignored.Add(int64(len(ignores))) mon.Dropped.Add(int64(len(dropped))) } func logPackageMatches(p pkg.Package, matches []match.Match) { if len(matches) == 0 { return } log.WithFields("package", displayPackage(p)).Debugf("found %d vulnerabilities", len(matches)) for idx, m := range matches { arm := selectArm(idx, len(matches)) log.WithFields("vuln", m.Vulnerability.ID, "namespace", m.Vulnerability.Namespace).Tracef(" %s", arm) } } func selectArm(idx, total int) string { if idx == total-1 { return leaf } return branch } func logExplicitDroppedPackageMatches(p pkg.Package, ignored []match.IgnoredMatch) { if len(ignored) == 0 { return } log.WithFields("package", displayPackage(p)).Debugf("dropped %d vulnerability matches due to hard-coded correction", len(ignored)) for idx, i := range ignored { arm := selectArm(idx, len(ignored)) log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules)).Tracef(" %s", arm) } } func logIgnoredMatches(ignored []match.IgnoredMatch) { if len(ignored) == 0 { return } log.Infof("ignored %d vulnerability matches", len(ignored)) for idx, i := range ignored { arm := selectArm(idx, len(ignored)) rule := "" if len(i.AppliedIgnoreRules) > 0 { rule = i.AppliedIgnoreRules[0].Reason if rule == "" { rule = i.AppliedIgnoreRules[0].Vulnerability } } vulnerability.LogDropped(i.Vulnerability.ID, "ignoreRules", rule, i) log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules), "package", displayPackage(i.Package)).Debugf(" %s", arm) } } type monitorWriter struct { PackagesProcessed *progress.Manual MatchesDiscovered *progress.Manual Fixed *progress.Manual Ignored *progress.Manual Dropped *progress.Manual BySeverity map[vulnerability.Severity]*progress.Manual } func newMonitor(pkgCount int) (monitorWriter, monitor.Matching) { manualBySev := make(map[vulnerability.Severity]*progress.Manual) for _, severity := range vulnerability.AllSeverities() { manualBySev[severity] = progress.NewManual(-1) } manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1) m := monitorWriter{ PackagesProcessed: progress.NewManual(int64(pkgCount)), MatchesDiscovered: progress.NewManual(-1), Fixed: progress.NewManual(-1), Ignored: progress.NewManual(-1), Dropped: progress.NewManual(-1), BySeverity: manualBySev, } monitorableBySev := make(map[vulnerability.Severity]progress.Monitorable) for sev, manual := range manualBySev { monitorableBySev[sev] = manual } return m, monitor.Matching{ PackagesProcessed: m.PackagesProcessed, MatchesDiscovered: m.MatchesDiscovered, Fixed: m.Fixed, Ignored: m.Ignored, Dropped: m.Dropped, BySeverity: monitorableBySev, } } func (m *monitorWriter) SetCompleted() { m.PackagesProcessed.SetCompleted() m.MatchesDiscovered.SetCompleted() m.Fixed.SetCompleted() m.Ignored.SetCompleted() m.Dropped.SetCompleted() for _, v := range m.BySeverity { v.SetCompleted() } } func trackMatcher(pkgCount int) *monitorWriter { writer, reader := newMonitor(pkgCount) bus.Publish(partybus.Event{ Type: event.VulnerabilityScanningStarted, Value: reader, }) return &writer } // eolTracker handles checking and caching EOL status for distros type eolTracker struct { checker vulnerability.EOLChecker cache map[string]eolCacheEntry } type eolCacheEntry struct { isEOL bool eolDate *time.Time } func newEOLTracker(enabled bool, provider vulnerability.Provider) *eolTracker { if !enabled { return &eolTracker{} } checker, ok := provider.(vulnerability.EOLChecker) if !ok { return &eolTracker{} } return &eolTracker{ checker: checker, cache: make(map[string]eolCacheEntry), } } // checkAndTrack checks if the package is from an EOL distro and returns true if so. // Results are cached per distro. func (t *eolTracker) checkAndTrack(p pkg.Package) bool { if t.checker == nil || p.Distro == nil { return false } distroKey := p.Distro.String() entry, checked := t.cache[distroKey] if !checked { eolDate, _, err := t.checker.GetOperatingSystemEOL(p.Distro) if err != nil { log.WithFields("distro", distroKey, "error", err).Debug("error checking EOL status") } entry = eolCacheEntry{ isEOL: eolDate != nil && eolDate.Before(time.Now()), eolDate: eolDate, } t.cache[distroKey] = entry } if entry.isEOL { log.WithFields("package", displayPackage(p), "distro", distroKey, "eol_date", entry.eolDate).Debug("package from EOL distro") return true } return false } ================================================ FILE: grype/vulnerability_matcher_test.go ================================================ package grype import ( "errors" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wagoodman/go-partybus" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" matcherMock "github.com/anchore/grype/grype/matcher/mock" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/pkg/qualifier" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vex" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/grype/internal/bus" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) func testVulnerabilities() []vulnerability.Vulnerability { return []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", Internal: vulnerability.Metadata{ Severity: "medium", }, }, PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-2013-fake-2", Namespace: "debian:distro:debian:8", }, PackageName: "neutron", Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat), }, { Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "github:language:ruby", Internal: vulnerability.Metadata{ Severity: "medium", }, }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), RelatedVulnerabilities: []vulnerability.Reference{ { ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, }, }, { Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", Internal: vulnerability.Metadata{ Severity: "critical", }, }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""), }, }, { Reference: vulnerability.Reference{ ID: "CVE-2014-fake-4", Namespace: "nvd:cpe", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""), }, }, { Reference: vulnerability.Reference{ ID: "CVE-2014-fake-5", Namespace: "nvd:cpe", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("= 4.0.1", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*", ""), }, }, { Reference: vulnerability.Reference{ ID: "CVE-2014-fake-6", Namespace: "nvd:cpe", }, PackageName: "activerecord", Constraint: version.MustGetConstraint("< 98SP3", version.UnknownFormat), CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*", ""), }, }, } } func Test_HasSeverityAtOrAbove(t *testing.T) { thePkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "the-package", Version: "v0.1", Type: syftPkg.RpmPkg, } matches := match.NewMatches() matches.Add(match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, }, Package: thePkg, Details: match.Details{ { Type: match.ExactDirectMatch, }, }, }) tests := []struct { name string failOnSeverity string matches match.Matches expectedResult bool }{ { name: "no-severity-set", failOnSeverity: "", matches: matches, expectedResult: false, }, { name: "below-threshold", failOnSeverity: "high", matches: matches, expectedResult: false, }, { name: "at-threshold", failOnSeverity: "medium", matches: matches, expectedResult: true, }, { name: "above-threshold", failOnSeverity: "low", matches: matches, expectedResult: true, }, } metadataProvider := mock.VulnerabilityProvider(testVulnerabilities()...) for _, test := range tests { t.Run(test.name, func(t *testing.T) { var failOnSeverity vulnerability.Severity if test.failOnSeverity != "" { sev := vulnerability.ParseSeverity(test.failOnSeverity) if sev == vulnerability.UnknownSeverity { t.Fatalf("could not parse severity") } failOnSeverity = sev } actual := hasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches) if test.expectedResult != actual { t.Errorf("expected: %v got : %v", test.expectedResult, actual) } }) } } func TestVulnerabilityMatcher_FindMatches(t *testing.T) { vp := mock.VulnerabilityProvider(testVulnerabilities()...) neutron2013Pkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "neutron", Version: "2013.1.1-1", Type: syftPkg.DebPkg, Distro: &distro.Distro{ Type: "debian", Version: "8", }, } mustCPE := func(c string) cpe.CPE { cp, err := cpe.New(c, "") if err != nil { t.Fatal(err) } return cp } activerecordPkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "activerecord", Version: "3.7.5", CPEs: []cpe.CPE{ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"), }, Type: syftPkg.GemPkg, Language: syftPkg.Ruby, } openvexProcessor, _ := vex.NewProcessor(vex.ProcessorOptions{ Documents: []string{ "vex/testdata/vex-docs/openvex-debian.json", }, IgnoreRules: []match.IgnoreRule{ { VexStatus: "fixed", }, }, }) type fields struct { Matchers []match.Matcher IgnoreRules []match.IgnoreRule FailSeverity *vulnerability.Severity NormalizeByCVE bool VexProcessor *vex.Processor } type args struct { pkgs []pkg.Package context pkg.Context } tests := []struct { name string fields fields args args wantMatches match.Matches wantIgnoredMatches []match.IgnoredMatch wantErr error }{ { name: "no matches", fields: fields{ Matchers: matcher.NewDefaultMatchers(matcher.Config{}), }, args: args{ pkgs: []pkg.Package{ { ID: pkg.ID(uuid.NewString()), Name: "neutrino", Version: "2099.1.1-1", Type: syftPkg.DebPkg, }, }, context: pkg.Context{}, }, }, { name: "matches by exact-direct match (OS)", fields: fields{ Matchers: matcher.NewDefaultMatchers(matcher.Config{}), }, args: args{ pkgs: []pkg.Package{ neutron2013Pkg, }, context: pkg.Context{}, }, wantMatches: match.NewMatches( match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: []cpe.CPE{}, Advisories: []vulnerability.Advisory{}, }, Package: neutron2013Pkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{Type: "debian", Version: "8"}, Namespace: "debian:distro:debian:8", Package: match.PackageParameter{Name: "neutron", Version: "2013.1.1-1"}, }, Found: match.DistroResult{ VulnerabilityID: "CVE-2014-fake-1", VersionConstraint: "< 2014.1.3-6 (deb)", }, Matcher: "dpkg-matcher", Confidence: 1, }, }, }, ), wantIgnoredMatches: nil, wantErr: nil, }, { name: "fail on severity threshold", fields: fields{ Matchers: matcher.NewDefaultMatchers(matcher.Config{}), FailSeverity: func() *vulnerability.Severity { x := vulnerability.LowSeverity return &x }(), }, args: args{ pkgs: []pkg.Package{ neutron2013Pkg, }, context: pkg.Context{}, }, wantMatches: match.NewMatches( match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: []cpe.CPE{}, Advisories: []vulnerability.Advisory{}, }, Package: neutron2013Pkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{Type: "debian", Version: "8"}, Namespace: "debian:distro:debian:8", Package: match.PackageParameter{Name: "neutron", Version: "2013.1.1-1"}, }, Found: match.DistroResult{ VulnerabilityID: "CVE-2014-fake-1", VersionConstraint: "< 2014.1.3-6 (deb)", }, Matcher: "dpkg-matcher", Confidence: 1, }, }, }, ), wantIgnoredMatches: nil, wantErr: grypeerr.ErrAboveSeverityThreshold, }, { name: "pass on severity threshold with VEX", fields: fields{ Matchers: matcher.NewDefaultMatchers(matcher.Config{}), FailSeverity: func() *vulnerability.Severity { x := vulnerability.LowSeverity return &x }(), VexProcessor: openvexProcessor, }, args: args{ pkgs: []pkg.Package{ neutron2013Pkg, }, context: pkg.Context{ Source: &source.Description{ Name: "debian", Version: "2013.1.1-1", Metadata: source.ImageMetadata{ RepoDigests: []string{ "debian@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", }, }, }, }, }, wantMatches: match.NewMatches(), wantIgnoredMatches: []match.IgnoredMatch{ { AppliedIgnoreRules: []match.IgnoreRule{ { Namespace: "vex", VexStatus: "fixed", }, }, Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "neutron", Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-1", Namespace: "debian:distro:debian:8", }, PackageQualifiers: []qualifier.Qualifier{}, CPEs: []cpe.CPE{}, Advisories: []vulnerability.Advisory{}, }, Package: neutron2013Pkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{Type: "debian", Version: "8"}, Namespace: "debian:distro:debian:8", Package: match.PackageParameter{Name: "neutron", Version: "2013.1.1-1"}, }, Found: match.DistroResult{ VulnerabilityID: "CVE-2014-fake-1", VersionConstraint: "< 2014.1.3-6 (deb)", }, Matcher: "dpkg-matcher", Confidence: 1, }, }, }, }, }, wantErr: nil, }, { name: "matches by exact-direct match (language)", fields: fields{ Matchers: matcher.NewDefaultMatchers(matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, }, }), }, args: args{ pkgs: []pkg.Package{ activerecordPkg, }, context: pkg.Context{}, }, wantMatches: match.NewMatches( match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, }, Package: activerecordPkg, Details: match.Details{ { Type: match.CPEMatch, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, Matcher: "ruby-gem-matcher", Confidence: 0.9, }, }, }, match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "github:language:ruby", }, RelatedVulnerabilities: []vulnerability.Reference{ { ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, CPEs: []cpe.CPE{}, }, Package: activerecordPkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{Name: "activerecord", Version: "3.7.5"}, }, Found: match.EcosystemResult{ VulnerabilityID: "GHSA-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", }, Matcher: "ruby-gem-matcher", Confidence: 1, }, }, }, ), wantIgnoredMatches: nil, wantErr: nil, }, { name: "normalize by cve", fields: fields{ Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, }, }, ), NormalizeByCVE: true, // IMPORTANT! }, args: args{ pkgs: []pkg.Package{ activerecordPkg, }, context: pkg.Context{}, }, wantMatches: match.NewMatches( match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: []vulnerability.Reference{ { ID: "GHSA-2014-fake-3", Namespace: "github:language:ruby", }, }, }, Package: activerecordPkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{Name: "activerecord", Version: "3.7.5"}, }, Found: match.EcosystemResult{ VulnerabilityID: "GHSA-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", }, Matcher: "ruby-gem-matcher", Confidence: 1, }, { Type: match.CPEMatch, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, Matcher: "ruby-gem-matcher", Confidence: 0.9, }, }, }, ), wantIgnoredMatches: nil, wantErr: nil, }, { name: "normalize by cve -- ignore GHSA", fields: fields{ Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, }, }, ), IgnoreRules: []match.IgnoreRule{ { Vulnerability: "GHSA-2014-fake-3", }, }, NormalizeByCVE: true, // IMPORTANT! }, args: args{ pkgs: []pkg.Package{ activerecordPkg, }, context: pkg.Context{}, }, wantMatches: match.NewMatches( match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, }, Package: activerecordPkg, Details: match.Details{ { Type: match.CPEMatch, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, Matcher: "ruby-gem-matcher", Confidence: 0.9, }, }, }, ), wantErr: nil, wantIgnoredMatches: []match.IgnoredMatch{ { Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{}, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: []vulnerability.Reference{ { ID: "GHSA-2014-fake-3", Namespace: "github:language:ruby", }, }, }, Package: activerecordPkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{Name: "activerecord", Version: "3.7.5"}, }, Found: match.EcosystemResult{ VulnerabilityID: "GHSA-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", }, Matcher: "ruby-gem-matcher", Confidence: 1, }, }, }, AppliedIgnoreRules: []match.IgnoreRule{ { Vulnerability: "GHSA-2014-fake-3", }, }, }, }, }, { name: "normalize by cve -- ignore CVE", fields: fields{ Matchers: matcher.NewDefaultMatchers( matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, }, }, ), IgnoreRules: []match.IgnoreRule{ { Vulnerability: "CVE-2014-fake-3", }, }, NormalizeByCVE: true, // IMPORTANT! }, args: args{ pkgs: []pkg.Package{ activerecordPkg, }, context: pkg.Context{}, }, wantMatches: match.NewMatches(), wantIgnoredMatches: []match.IgnoredMatch{ { Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: nil, }, Package: activerecordPkg, Details: match.Details{ { Type: match.CPEMatch, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, Matcher: "ruby-gem-matcher", Confidence: 0.9, }, }, }, AppliedIgnoreRules: []match.IgnoreRule{ { Vulnerability: "CVE-2014-fake-3", }, }, }, { AppliedIgnoreRules: []match.IgnoreRule{ { Vulnerability: "CVE-2014-fake-3", }, }, Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{}, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: []vulnerability.Reference{ { ID: "GHSA-2014-fake-3", Namespace: "github:language:ruby", }, }, }, Package: activerecordPkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{Name: "activerecord", Version: "3.7.5"}, }, Found: match.EcosystemResult{ VulnerabilityID: "GHSA-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", }, Matcher: "ruby-gem-matcher", Confidence: 1, }, }, }, }, }, wantErr: nil, }, { name: "ignore CVE (not normalized by CVE)", fields: fields{ Matchers: matcher.NewDefaultMatchers(matcher.Config{ Ruby: ruby.MatcherConfig{ UseCPEs: true, }, }), IgnoreRules: []match.IgnoreRule{ { Vulnerability: "CVE-2014-fake-3", }, }, }, args: args{ pkgs: []pkg.Package{ activerecordPkg, }, }, wantMatches: match.NewMatches( match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "GHSA-2014-fake-3", Namespace: "github:language:ruby", }, RelatedVulnerabilities: []vulnerability.Reference{ { ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, CPEs: []cpe.CPE{}, }, Package: activerecordPkg, Details: match.Details{ { Type: match.ExactDirectMatch, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{Name: "activerecord", Version: "3.7.5"}, }, Found: match.EcosystemResult{ VulnerabilityID: "GHSA-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", }, Matcher: "ruby-gem-matcher", Confidence: 1, }, }, }, ), wantIgnoredMatches: []match.IgnoredMatch{ { AppliedIgnoreRules: []match.IgnoreRule{ { Vulnerability: "CVE-2014-fake-3", }, }, Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ PackageName: "activerecord", Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat), Reference: vulnerability.Reference{ ID: "CVE-2014-fake-3", Namespace: "nvd:cpe", }, CPEs: []cpe.CPE{ mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"), }, PackageQualifiers: []qualifier.Qualifier{}, Advisories: []vulnerability.Advisory{}, }, Package: activerecordPkg, Details: match.Details{ { Type: match.CPEMatch, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*", }, Package: match.PackageParameter{ Name: "activerecord", Version: "3.7.5", }, }, Found: match.CPEResult{ VulnerabilityID: "CVE-2014-fake-3", VersionConstraint: "< 3.7.6 (unknown)", CPEs: []string{ "cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", }, }, Matcher: "ruby-gem-matcher", Confidence: 0.9, }, }, }, }, }, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &VulnerabilityMatcher{ VulnerabilityProvider: vp, Matchers: tt.fields.Matchers, IgnoreRules: tt.fields.IgnoreRules, FailSeverity: tt.fields.FailSeverity, NormalizeByCVE: tt.fields.NormalizeByCVE, VexProcessor: tt.fields.VexProcessor, } listener := &busListener{} bus.Set(listener) defer bus.Set(nil) actualMatches, actualIgnoreMatches, err := m.FindMatches(tt.args.pkgs, tt.args.context) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) return } else if err != nil { t.Errorf("FindMatches() error = %v, wantErr %v", err, tt.wantErr) return } var opts = []cmp.Option{ cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(match.Match{}), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"), cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"), cmpopts.IgnoreUnexported(match.IgnoredMatch{}), } if d := cmp.Diff(tt.wantMatches.Sorted(), actualMatches.Sorted(), opts...); d != "" { t.Errorf("FindMatches() matches mismatch [ha!] (-want +got):\n%s", d) } if d := cmp.Diff(tt.wantIgnoredMatches, actualIgnoreMatches, opts...); d != "" { t.Errorf("FindMatches() ignored matches mismatch [ha!] (-want +got):\n%s", d) } // validate the bus-reported ignored counts are accurate require.Equal(t, int64(len(tt.wantIgnoredMatches)), listener.matching.Ignored.Current()) }) } } func Test_fatalErrors(t *testing.T) { tests := []struct { name string matcherFunc matcherMock.MatchFunc assertErr assert.ErrorAssertionFunc }{ { name: "no error", matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return nil, nil, nil }, assertErr: assert.NoError, }, { name: "non-fatal error", matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return nil, nil, errors.New("some error") }, assertErr: assert.NoError, }, { name: "fatal error", matcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { return nil, nil, match.NewFatalError(match.UnknownMatcherType, errors.New("some error")) }, assertErr: assert.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &VulnerabilityMatcher{ Matchers: []match.Matcher{matcherMock.New(syftPkg.JavaPkg, tt.matcherFunc)}, } _, _, err := m.FindMatches([]pkg.Package{ { Name: "foo", Version: "1.2.3", Type: syftPkg.JavaPkg, }, }, pkg.Context{}, ) tt.assertErr(t, err) }) } } func Test_matchIgnoreFiltering(t *testing.T) { // one commonly used filter uses APK NAK data to exclude false positives on language packages at the same locations // based on packages in the APK DB. for example: // APK package pkg1 is not vulnerable, but another python package is found with a slightly different name // by the python cataloger when it scans the same files at the same locations that were associated with // the APK package. the python package is checked by a separate matcher, so we surface ignore rules // based on location and vuln id to exclude these false positives ignoreByLocationAndVuln := func(locationToVulnIDs map[string][]string) []match.IgnoreFilter { var out []match.IgnoreFilter for path, vulnIDs := range locationToVulnIDs { for _, vulnID := range vulnIDs { out = append(out, match.IgnoreRule{ Vulnerability: vulnID, IncludeAliases: true, Package: match.IgnoreRulePackage{ Location: path, }, }) } } return out } // with the addition of unaffected packages, ignore rules are returned by package language searches to account for // searches for subsequent CPE searches based on the specific package ignoreByPackageNameAndVuln := func(pkgNameToVulnIDs map[string][]string) []match.IgnoreFilter { var out []match.IgnoreFilter for packageName, vulnIDs := range pkgNameToVulnIDs { for _, vulnID := range vulnIDs { out = append(out, match.IgnoreRule{ Vulnerability: vulnID, IncludeAliases: true, Package: match.IgnoreRulePackage{ Name: packageName, }, }) } } return out } // construct matches here so we can make test cases more readable loc1 := "/usr/bin/pkg1" loc2 := "/other/pkg1" vuln1 := "vuln1" vuln2 := "vuln2" pkg1_vuln1_loc1 := match.Match{ Package: pkg.Package{ Type: syftPkg.PythonPkg, Name: "pkg1", Locations: file.NewLocationSet(file.NewLocation(loc1)), }, Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}}, } pkg1_vuln2_loc1 := match.Match{ Package: pkg.Package{ Name: "pkg1", Locations: file.NewLocationSet(file.NewLocation(loc1)), }, Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln2}}, } pkg1_vuln1_loc2 := match.Match{ Package: pkg.Package{ Name: "pkg1", Locations: file.NewLocationSet(file.NewLocation(loc2)), }, Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}}, } pkg2_vuln1_loc1 := match.Match{ Package: pkg.Package{ Type: syftPkg.PythonPkg, Name: "pkg2", Locations: file.NewLocationSet(file.NewLocation(loc1)), }, Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}}, } pkg2_vuln2_loc1 := match.Match{ Package: pkg.Package{ Name: "pkg2", Locations: file.NewLocationSet(file.NewLocation(loc1)), }, Vulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln2}}, } cases := []struct { name string inputMatches []match.Match ignoreFilters []match.IgnoreFilter expected []match.Match }{ { name: "no input matches", inputMatches: nil, ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ loc1: {vuln1}, }), expected: nil, }, { name: "no ignore rules", inputMatches: []match.Match{ pkg1_vuln1_loc1, pkg1_vuln1_loc2, }, ignoreFilters: nil, expected: []match.Match{ pkg1_vuln1_loc1, pkg1_vuln1_loc2, }, }, { name: "happy path filtering", inputMatches: []match.Match{ pkg1_vuln1_loc1, }, ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ loc1: {vuln1}, }), expected: nil, }, { name: "location match different vuln", inputMatches: []match.Match{ pkg1_vuln1_loc1, }, ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ loc1: {vuln2}, }), expected: []match.Match{ pkg1_vuln1_loc1, }, }, { name: "location across packages", inputMatches: []match.Match{ pkg1_vuln1_loc1, pkg1_vuln2_loc1, pkg2_vuln1_loc1, pkg2_vuln2_loc1, }, ignoreFilters: ignoreByLocationAndVuln(map[string][]string{ loc1: {vuln1}, }), expected: []match.Match{ pkg1_vuln2_loc1, pkg2_vuln2_loc1, }, }, { name: "package name", inputMatches: []match.Match{ pkg1_vuln1_loc1, pkg1_vuln2_loc1, pkg2_vuln1_loc1, }, ignoreFilters: ignoreByPackageNameAndVuln(map[string][]string{ pkg1_vuln1_loc1.Package.Name: {vuln1}, }), expected: []match.Match{ pkg1_vuln2_loc1, pkg2_vuln1_loc1, }, }, { name: "not indexed rule", inputMatches: []match.Match{ pkg1_vuln1_loc1, pkg1_vuln2_loc1, // not python package pkg2_vuln1_loc1, }, ignoreFilters: []match.IgnoreFilter{ match.IgnoreRule{ Package: match.IgnoreRulePackage{ Type: string(syftPkg.PythonPkg), // no indexed properties }, }, }, expected: []match.Match{ pkg1_vuln2_loc1, }, }, { name: "not indexed filter", inputMatches: []match.Match{ pkg1_vuln1_loc1, pkg1_vuln1_loc2, pkg2_vuln1_loc1, pkg1_vuln2_loc1, }, ignoreFilters: []match.IgnoreFilter{ testIgnoreFilter{func(m match.Match) bool { return m.Vulnerability.ID == vuln1 }}, }, expected: []match.Match{ pkg1_vuln2_loc1, }, }, { name: "multiple rules mixed", inputMatches: []match.Match{ pkg1_vuln1_loc1, // removed by custom filter pkg1_vuln1_loc2, pkg2_vuln1_loc1, // removed by name + vuln pkg1_vuln2_loc1, // removed by location + vuln }, ignoreFilters: append(append([]match.IgnoreFilter{ testIgnoreFilter{func(m match.Match) bool { return m.Vulnerability.ID == vuln1 }}, }, ignoreByLocationAndVuln(map[string][]string{ loc2: {vuln2}, })...), ignoreByPackageNameAndVuln(map[string][]string{ pkg2_vuln1_loc1.Package.Name: {vuln1}, })...), expected: []match.Match{ pkg1_vuln2_loc1, }, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { filter := ignoredMatchFilter(tt.ignoreFilters) actual, _ := match.ApplyIgnoreFilters(tt.inputMatches, filter) assert.Equal(t, tt.expected, actual) }) } } func Test_ignoredMatchFilterReasons(t *testing.T) { matches := []match.Match{ { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-123", }, }, Package: pkg.Package{ Locations: file.NewLocationSet(file.NewLocation("/usr/bin/thing")), }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-456", }, }, }, { Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-789", }, Status: "filter-me", }, }, } ignores := []match.IgnoreFilter{ match.IgnoreRule{ Reason: "test-location-ignore-rule", Package: match.IgnoreRulePackage{ Location: "/usr/bin/thing", }, }, testIgnoreFilter{ f: func(m match.Match) bool { return m.Vulnerability.Status == "filter-me" }, }, } f := ignoredMatchFilter(ignores) var ignoredReasons []string for _, m := range matches { got := f.IgnoreMatch(m) for _, r := range got { ignoredReasons = append(ignoredReasons, r.Reason) } } require.ElementsMatch(t, []string{"test-location-ignore-rule", "test-filtered"}, ignoredReasons) } type testIgnoreFilter struct { f func(match.Match) bool } func (t testIgnoreFilter) IgnoreMatch(m match.Match) []match.IgnoreRule { if t.f(m) { return []match.IgnoreRule{ { Reason: "test-filtered", }, } } return nil } type panicyMatcher struct { matcherType match.MatcherType } func (m *panicyMatcher) PackageTypes() []syftPkg.Type { return nil } func (m *panicyMatcher) Type() match.MatcherType { return m.matcherType } func (m *panicyMatcher) Match(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) { panic("test panic message") } func TestCallMatcherSafely_RecoverFromPanic(t *testing.T) { matcher := &panicyMatcher{ matcherType: "test-matcher", } _, _, err := callMatcherSafely(matcher, nil, pkg.Package{}) require.Error(t, err) assert.True(t, match.IsFatalError(err)) require.Contains(t, err.Error(), "test panic message", "missing message") require.Contains(t, err.Error(), "test-matcher", "missing matcher name") } type busListener struct { matching monitor.Matching } func (b *busListener) Publish(e partybus.Event) { if e.Type == event.VulnerabilityScanningStarted { if m, ok := e.Value.(monitor.Matching); ok { b.matching = m } } } var _ partybus.Publisher = (*busListener)(nil) // mockEOLProvider is a mock vulnerability provider that also implements EOLChecker type mockEOLProvider struct { vulnerability.Provider eolDates map[string]*time.Time // distro key -> EOL date } func (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (*time.Time, *time.Time, error) { if m.eolDates == nil { return nil, nil, nil } eolDate := m.eolDates[d.String()] return eolDate, nil, nil } var _ vulnerability.EOLChecker = (*mockEOLProvider)(nil) func TestEOLTracker(t *testing.T) { pastDate := time.Now().Add(-24 * time.Hour) futureDate := time.Now().Add(24 * time.Hour * 365) debian8 := &distro.Distro{ Type: "debian", Version: "8", } ubuntu2204 := &distro.Distro{ Type: "ubuntu", Version: "22.04", } tests := []struct { name string enabled bool provider vulnerability.Provider pkg pkg.Package expectedIsEOL bool }{ { name: "tracking disabled - returns false", enabled: false, provider: mock.VulnerabilityProvider(), pkg: pkg.Package{Distro: debian8}, expectedIsEOL: false, }, { name: "provider does not implement EOLChecker - returns false", enabled: true, provider: mock.VulnerabilityProvider(), // does not implement EOLChecker pkg: pkg.Package{Distro: debian8}, expectedIsEOL: false, }, { name: "package has no distro - returns false", enabled: true, provider: &mockEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDates: map[string]*time.Time{ debian8.String(): &pastDate, }, }, pkg: pkg.Package{Distro: nil}, expectedIsEOL: false, }, { name: "distro is EOL - returns true", enabled: true, provider: &mockEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDates: map[string]*time.Time{ debian8.String(): &pastDate, }, }, pkg: pkg.Package{Distro: debian8}, expectedIsEOL: true, }, { name: "distro is not EOL - returns false", enabled: true, provider: &mockEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDates: map[string]*time.Time{ ubuntu2204.String(): &futureDate, }, }, pkg: pkg.Package{Distro: ubuntu2204}, expectedIsEOL: false, }, { name: "distro has no EOL data - returns false", enabled: true, provider: &mockEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDates: map[string]*time.Time{}, // no data for any distro }, pkg: pkg.Package{Distro: debian8}, expectedIsEOL: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tracker := newEOLTracker(tt.enabled, tt.provider) result := tracker.checkAndTrack(tt.pkg) assert.Equal(t, tt.expectedIsEOL, result) }) } } func TestEOLTrackerCaching(t *testing.T) { pastDate := time.Now().Add(-24 * time.Hour) debian8 := &distro.Distro{ Type: "debian", Version: "8", } callCount := 0 provider := &countingEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDates: map[string]*time.Time{ debian8.String(): &pastDate, }, callCount: &callCount, } tracker := newEOLTracker(true, provider) // First call should query the provider result1 := tracker.checkAndTrack(pkg.Package{Distro: debian8, Name: "pkg1"}) assert.True(t, result1) assert.Equal(t, 1, callCount, "expected one call to GetOperatingSystemEOL") // Second call with same distro should use cache result2 := tracker.checkAndTrack(pkg.Package{Distro: debian8, Name: "pkg2"}) assert.True(t, result2) assert.Equal(t, 1, callCount, "expected no additional call - should use cache") // Third call with same distro should still use cache result3 := tracker.checkAndTrack(pkg.Package{Distro: debian8, Name: "pkg3"}) assert.True(t, result3) assert.Equal(t, 1, callCount, "expected no additional call - should use cache") } type countingEOLProvider struct { vulnerability.Provider eolDates map[string]*time.Time callCount *int } func (c *countingEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (*time.Time, *time.Time, error) { *c.callCount++ if c.eolDates == nil { return nil, nil, nil } eolDate := c.eolDates[d.String()] return eolDate, nil, nil } var _ vulnerability.EOLChecker = (*countingEOLProvider)(nil) func TestVulnerabilityMatcher_EOLDistroPackages(t *testing.T) { pastDate := time.Now().Add(-24 * time.Hour) futureDate := time.Now().Add(24 * time.Hour * 365) debian8 := &distro.Distro{ Type: "debian", Version: "8", } ubuntu2204 := &distro.Distro{ Type: "ubuntu", Version: "22.04", } eolPkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "eol-pkg", Version: "1.0.0", Type: syftPkg.DebPkg, Distro: debian8, } nonEOLPkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "non-eol-pkg", Version: "1.0.0", Type: syftPkg.DebPkg, Distro: ubuntu2204, } noDistroPkg := pkg.Package{ ID: pkg.ID(uuid.NewString()), Name: "no-distro-pkg", Version: "1.0.0", Type: syftPkg.NpmPkg, } provider := &mockEOLProvider{ Provider: mock.VulnerabilityProvider(), eolDates: map[string]*time.Time{ debian8.String(): &pastDate, ubuntu2204.String(): &futureDate, }, } tests := []struct { name string alertsConfig AlertsConfig packages []pkg.Package expectedEOLPackages []string // package names expected to be tracked as EOL }{ { name: "EOL tracking enabled - tracks EOL packages", alertsConfig: AlertsConfig{ EnableEOLDistroWarnings: true, }, packages: []pkg.Package{eolPkg, nonEOLPkg, noDistroPkg}, expectedEOLPackages: []string{"eol-pkg"}, }, { name: "EOL tracking disabled - no packages tracked", alertsConfig: AlertsConfig{ EnableEOLDistroWarnings: false, }, packages: []pkg.Package{eolPkg, nonEOLPkg, noDistroPkg}, expectedEOLPackages: nil, }, { name: "all packages from EOL distro", alertsConfig: AlertsConfig{ EnableEOLDistroWarnings: true, }, packages: []pkg.Package{eolPkg}, expectedEOLPackages: []string{"eol-pkg"}, }, { name: "no packages from EOL distro", alertsConfig: AlertsConfig{ EnableEOLDistroWarnings: true, }, packages: []pkg.Package{nonEOLPkg, noDistroPkg}, expectedEOLPackages: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &VulnerabilityMatcher{ VulnerabilityProvider: provider, Matchers: matcher.NewDefaultMatchers(matcher.Config{}), Alerts: tt.alertsConfig, } listener := &busListener{} bus.Set(listener) defer bus.Set(nil) _, _, err := m.FindMatches(tt.packages, pkg.Context{}) require.NoError(t, err) eolPackages := m.EOLDistroPackages() var actualNames []string for _, p := range eolPackages { actualNames = append(actualNames, p.Name) } if tt.expectedEOLPackages == nil { assert.Empty(t, actualNames) } else { assert.ElementsMatch(t, tt.expectedEOLPackages, actualNames) } }) } } ================================================ FILE: install.sh ================================================ #!/bin/sh # note: we require errors to propagate (don't set -e) set -u PROJECT_NAME=grype OWNER=anchore REPO="${PROJECT_NAME}" GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download INSTALL_SH_BASE_URL=https://get.anchore.io/${PROJECT_NAME} LEGACY_INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME} PROGRAM_ARGS=$@ # signature verification options # the location to the cosign binary (allowed to be overridden by the user) COSIGN_BINARY=${COSIGN_BINARY:-cosign} VERIFY_SIGN=false # this is the earliest tag in the repo where cosign sign-blob was introduced in the release process (see the goreleaser config) VERIFY_SIGN_SUPPORTED_VERSION=v0.72.0 # this is the earliest tag in the repo where the -v flag was introduced to this install.sh script VERIFY_SIGN_FLAG_VERSION=v0.79.0 # do not change the name of this parameter (this must always be backwards compatible) DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true} # ------------------------------------------------------------------------ # https://github.com/client9/shlib - portable posix shell functions # Public domain - http://unlicense.org # https://github.com/client9/shlib/blob/master/LICENSE.md # but credit (and pull requests) appreciated. # ------------------------------------------------------------------------ is_command() ( command -v "$1" >/dev/null ) echo_stderr() ( echo "$@" 1>&2 ) _logp=2 log_set_priority() { _logp="$1" } log_priority() ( if test -z "$1"; then echo "$_logp" return fi [ "$1" -le "$_logp" ] ) init_colors() { RED='' BLUE='' PURPLE='' BOLD='' RESET='' # check if stdout is a terminal if test -t 1 && is_command tput; then # see if it supports colors ncolors=$(tput colors) if test -n "$ncolors" && test $ncolors -ge 8; then RED='\033[0;31m' BLUE='\033[0;34m' PURPLE='\033[0;35m' BOLD='\033[1m' RESET='\033[0m' fi fi } init_colors log_tag() ( case $1 in 0) echo "${RED}${BOLD}[error]${RESET}" ;; 1) echo "${RED}[warn]${RESET}" ;; 2) echo "[info]${RESET}" ;; 3) echo "${BLUE}[debug]${RESET}" ;; 4) echo "${PURPLE}[trace]${RESET}" ;; *) echo "[$1]" ;; esac ) log_trace_priority=4 log_trace() ( priority=$log_trace_priority log_priority "$priority" || return 0 echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" ) log_debug_priority=3 log_debug() ( priority=$log_debug_priority log_priority "$priority" || return 0 echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" ) log_info_priority=2 log_info() ( priority=$log_info_priority log_priority "$priority" || return 0 echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" ) log_warn_priority=1 log_warn() ( priority=$log_warn_priority log_priority "$priority" || return 0 echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" ) log_err_priority=0 log_err() ( priority=$log_err_priority log_priority "$priority" || return 0 echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" ) uname_os_check() ( os=$1 case "$os" in darwin) return 0 ;; dragonfly) return 0 ;; freebsd) return 0 ;; linux) return 0 ;; android) return 0 ;; nacl) return 0 ;; netbsd) return 0 ;; openbsd) return 0 ;; plan9) return 0 ;; solaris) return 0 ;; windows) return 0 ;; esac log_err "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" return 1 ) uname_arch_check() ( arch=$1 case "$arch" in 386) return 0 ;; amd64) return 0 ;; arm64) return 0 ;; armv5) return 0 ;; armv6) return 0 ;; armv7) return 0 ;; ppc64) return 0 ;; ppc64le) return 0 ;; mips) return 0 ;; mipsle) return 0 ;; mips64) return 0 ;; mips64le) return 0 ;; s390x) return 0 ;; amd64p32) return 0 ;; esac log_err "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" return 1 ) unpack() ( archive=$1 log_trace "unpack(archive=${archive})" case "${archive}" in *.tar.gz | *.tgz) tar --no-same-owner -xzf "${archive}" ;; *.tar) tar --no-same-owner -xf "${archive}" ;; *.zip) unzip -q "${archive}" ;; *.dmg) extract_from_dmg "${archive}" ;; *) log_err "unpack unknown archive format for ${archive}" return 1 ;; esac ) extract_from_dmg() ( dmg_file=$1 mount_point="/Volumes/tmp-dmg" hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}" cp -fR "${mount_point}/." ./ hdiutil detach -quiet -force "${mount_point}" ) http_download_curl() ( local_file=$1 source_url=$2 header=$3 log_trace "http_download_curl(local_file=$local_file, source_url=$source_url, header=$header)" if [ -z "$header" ]; then code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") else code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") fi if [ "$code" != "200" ]; then log_err "received HTTP status=$code for url='$source_url'" return 1 fi return 0 ) http_download_wget() ( local_file=$1 source_url=$2 header=$3 log_trace "http_download_wget(local_file=$local_file, source_url=$source_url, header=$header)" if [ -z "$header" ]; then wget -q -O "$local_file" "$source_url" else wget -q --header "$header" -O "$local_file" "$source_url" fi ) http_download() ( log_debug "http_download(url=$2)" if is_command curl; then http_download_curl "$@" return elif is_command wget; then http_download_wget "$@" return fi log_err "http_download unable to find wget or curl" return 1 ) http_copy() ( tmp=$(mktemp) http_download "${tmp}" "$1" "$2" || return 1 body=$(cat "$tmp") rm -f "${tmp}" echo "$body" ) hash_sha256() ( TARGET=${1:-/dev/stdin} if is_command gsha256sum; then hash=$(gsha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command sha256sum; then hash=$(sha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command shasum; then hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command openssl; then hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f a else log_err "hash_sha256 unable to find command to compute sha-256 hash" return 1 fi ) hash_sha256_verify() ( target=$1 checksums=$2 if [ -z "$checksums" ]; then log_err "hash_sha256_verify checksum file not specified as argument" return 1 fi target_basename=${target##*/} want=$(grep "${target_basename}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) if [ -z "$want" ]; then log_err "hash_sha256_verify unable to find checksum for '${target}' in '${checksums}'" return 1 fi got=$(hash_sha256 "$target") if [ "$want" != "$got" ]; then log_err "hash_sha256_verify checksum for '$target' did not verify ${want} vs $got" return 1 fi ) # ------------------------------------------------------------------------ # End of functions from https://github.com/client9/shlib # ------------------------------------------------------------------------ # asset_file_exists [path] # # returns 1 if the given file does not exist # asset_file_exists() ( path="$1" if [ ! -f "${path}" ]; then return 1 fi ) # github_release_json [owner] [repo] [version] # # outputs release json string # github_release_json() ( owner=$1 repo=$2 version=$3 test -z "$version" && version="latest" giturl="https://github.com/${owner}/${repo}/releases/${version}" json=$(http_copy "$giturl" "Accept:application/json") log_trace "github_release_json(owner=${owner}, repo=${repo}, version=${version}) returned '${json}'" test -z "$json" && return 1 echo "${json}" ) # extract_value [key-value-pair] # # outputs value from a colon delimited key-value pair # extract_value() ( key_value="$1" IFS=':' read -r _ value << EOF ${key_value} EOF echo "$value" ) # extract_json_value [json] [key] # # outputs value of the key from the given json string # extract_json_value() ( json="$1" key="$2" key_value=$(echo "${json}" | grep -o "\"$key\":[^,]*[,}]" | tr -d '",}') extract_value "$key_value" ) # github_release_tag [release-json] # # outputs release tag string # github_release_tag() ( json="$1" tag=$(extract_json_value "${json}" "tag_name") test -z "$tag" && return 1 echo "$tag" ) # github_release_asset_url [release-url-prefix] [name] [version] [output-dir] [filename] # # outputs the url to the release asset # github_release_asset_url() ( download_url="$1" name="$2" version="$3" filename="$4" complete_filename="${name}_${version}_${filename}" complete_url="${download_url}/${complete_filename}" echo "${complete_url}" ) # download_github_release_checksums_files [release-url-prefix] [name] [version] [output-dir] [filename] # # outputs path to the downloaded checksums related file # download_github_release_checksums_files() ( download_url="$1" name="$2" version="$3" output_dir="$4" filename="$5" log_trace "download_github_release_checksums_files(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir}, filename=${filename})" complete_filename="${name}_${version}_${filename}" complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}") output_path="${output_dir}/${complete_filename}" http_download "${output_path}" "${complete_url}" "" asset_file_exists "${output_path}" log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'" echo "${output_path}" ) # download_github_release_checksums [release-url-prefix] [name] [version] [output-dir] # # outputs path to the downloaded checksums file # download_github_release_checksums() ( download_github_release_checksums_files "$@" "checksums.txt" ) # github_release_checksums_sig_url [release-url-prefix] [name] [version] # # outputs the url to the release checksums signature file # github_release_checksums_sig_url() ( github_release_asset_url "$@" "checksums.txt.sig" ) # github_release_checksums_cert_url [release-url-prefix] [name] [version] # # outputs the url to the release checksums certificate file # github_release_checksums_cert_url() ( github_release_asset_url "$@" "checksums.txt.pem" ) # search_for_asset [checksums-file-path] [name] [os] [arch] [format] # # outputs name of the asset to download # search_for_asset() ( checksum_path="$1" name="$2" os="$3" arch="$4" format="$5" log_trace "search_for_asset(checksum-path=${checksum_path}, name=${name}, os=${os}, arch=${arch}, format=${format})" asset_glob="${name}_.*_${os}_${arch}.${format}" output_path=$(grep -o "${asset_glob}" "${checksum_path}" || true) log_trace "search_for_asset() returned '${output_path}'" echo "${output_path}" ) # uname_os # # outputs an adjusted os value # uname_os() ( os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in cygwin_nt*) os="windows" ;; mingw*) os="windows" ;; msys_nt*) os="windows" ;; esac uname_os_check "$os" log_trace "uname_os() returned '${os}'" echo "$os" ) # uname_arch # # outputs an adjusted architecture value # uname_arch() ( arch=$(uname -m) case $arch in x86_64) arch="amd64" ;; x86) arch="386" ;; i686) arch="386" ;; i386) arch="386" ;; aarch64) arch="arm64" ;; armv5*) arch="armv5" ;; armv6*) arch="armv6" ;; armv7*) arch="armv7" ;; esac uname_arch_check "${arch}" log_trace "uname_arch() returned '${arch}'" echo "${arch}" ) # get_release_tag [owner] [repo] [tag] # # outputs tag string # get_release_tag() ( owner="$1" repo="$2" tag="$3" log_trace "get_release_tag(owner=${owner}, repo=${repo}, tag=${tag})" json=$(github_release_json "${owner}" "${repo}" "${tag}") real_tag=$(github_release_tag "${json}") if test -z "${real_tag}"; then return 1 fi log_trace "get_release_tag() returned '${real_tag}'" echo "${real_tag}" ) # tag_to_version [tag] # # outputs version string # tag_to_version() ( tag="$1" value="${tag#v}" log_trace "tag_to_version(tag=${tag}) returned '${value}'" echo "$value" ) # get_binary_name [os] [arch] [default-name] # # outputs a the binary string name # get_binary_name() ( os="$1" arch="$2" binary="$3" original_binary="${binary}" case "${os}" in windows) binary="${binary}.exe" ;; esac log_trace "get_binary_name(os=${os}, arch=${arch}, binary=${original_binary}) returned '${binary}'" echo "${binary}" ) # get_format_name [os] [arch] [default-format] # # outputs an adjusted file format # get_format_name() ( os="$1" arch="$2" format="$3" original_format="${format}" case ${os} in windows) format=zip ;; esac log_trace "get_format_name(os=${os}, arch=${arch}, format=${original_format}) returned '${format}'" echo "${format}" ) # download_and_install_asset [release-url-prefix] [download-path] [install-path] [name] [os] [arch] [version] [format] [binary] # # attempts to download the archive and install it to the given path. # download_and_install_asset() ( download_url="$1" download_path="$2" install_path=$3 name="$4" os="$5" arch="$6" version="$7" format="$8" binary="$9" if ! asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}"); then log_err "could not download asset for os='${os}' arch='${arch}' format='${format}'" return 1 fi # don't continue if we couldn't download an asset if [ -z "${asset_filepath}" ]; then log_err "could not find release asset for os='${os}' arch='${arch}' format='${format}' " return 1 fi install_asset "${asset_filepath}" "${install_path}" "${binary}" ) # verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version] # # attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch. # verify_sign() { checksums_file=$1 cert_reference=$2 sig_reference=$3 log_trace "verifying artifact $1" log_file=$(mktemp) ${COSIGN_BINARY} \ verify-blob "$checksums_file" \ --certificate "$cert_reference" \ --signature "$sig_reference" \ --certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1 if [ $? -ne 0 ]; then log_err "$(cat "${log_file}")" rm -f "${log_file}" return 1 fi rm -f "${log_file}" } # download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary] # # outputs the path to the downloaded asset asset_filepath # download_asset() ( download_url="$1" destination="$2" name="$3" os="$4" arch="$5" version="$6" format="$7" log_trace "download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})" checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}") log_trace "checksums content:\n$(cat ${checksums_filepath})" asset_filename=$(search_for_asset "${checksums_filepath}" "${name}" "${os}" "${arch}" "${format}") # don't continue if we couldn't find a matching asset from the checksums file if [ -z "${asset_filename}" ]; then return 1 fi if [ "$VERIFY_SIGN" = true ]; then checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}") log_trace "checksums signature url: ${checksum_sig_file_url}" checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}") log_trace "checksums certificate url: ${checksums_cert_file_url}" if ! verify_sign "${checksums_filepath}" "${checksums_cert_file_url}" "${checksum_sig_file_url}"; then log_err "signature verification failed" return 1 fi log_info "signature verification succeeded" fi asset_url="${download_url}/${asset_filename}" asset_filepath="${destination}/${asset_filename}" http_download "${asset_filepath}" "${asset_url}" "" hash_sha256_verify "${asset_filepath}" "${checksums_filepath}" log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'" echo "${asset_filepath}" ) # install_asset [asset-path] [destination-path] [binary] # install_asset() ( asset_filepath="$1" destination="$2" binary="$3" log_trace "install_asset(asset=${asset_filepath}, destination=${destination}, binary=${binary})" # don't continue if we don't have anything to install if [ -z "${asset_filepath}" ]; then return fi archive_dir=$(dirname "${asset_filepath}") # unarchive the downloaded archive to the temp dir (cd "${archive_dir}" && unpack "${asset_filepath}") # create the destination dir test ! -d "${destination}" && install -d "${destination}" # install the binary to the destination dir install "${archive_dir}/${binary}" "${destination}/" ) # compare two semver strings. Returns 0 if version1 >= version2, 1 otherwise. # Note: pre-release (-) and metadata (+) are not supported. compare_semver() { # remove leading 'v' if present version1=${1#v} version2=${2#v} IFS=. read -r major1 minor1 patch1 <= '$VERIFY_SIGN_SUPPORTED_VERSION')" log_err "aborting installation" return 1 else log_trace "${PROJECT_NAME} release '$version' supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')" fi # will invoking an earlier version of this script work (considering the -v flag)? if ! compare_semver "$version" "$VERIFY_SIGN_FLAG_VERSION"; then # the -v argument did not always exist, so we cannot be guaranteed that invoking an earlier version of this script # will work (error with "illegal option -v"). However, the user requested signature verification, so we will # attempt to install the application with this version of the script (keeping signature verification). DOWNLOAD_TAG_INSTALL_SCRIPT=false log_debug "provided version install script does not support -v flag (>= '$VERIFY_SIGN_FLAG_VERSION'), using current script for installation" else log_trace "provided version install script supports -v flag (>= '$VERIFY_SIGN_FLAG_VERSION')" fi # check to see if the cosign binary is installed if is_command "${COSIGN_BINARY}"; then log_trace "${COSIGN_BINARY} binary is installed" else log_err "signature verification is requested but ${COSIGN_BINARY} binary is not installed (see https://docs.sigstore.dev/cosign/system_config/installation/ to install it)" return 1 fi } main() ( # parse arguments # note: never change default install directory (this must always be backwards compatible) install_dir=${install_dir:-./bin} # note: never change the program flags or arguments (this must always be backwards compatible) while getopts "b:dvh?x" arg; do case "$arg" in b) install_dir="$OPTARG" ;; d) if [ "$_logp" = "$log_info_priority" ]; then # -d == debug log_set_priority $log_debug_priority else # -dd (or -ddd...) == trace log_set_priority $log_trace_priority fi ;; v) VERIFY_SIGN=true;; h | \?) cat <= 10.0: return vulnerability.UnknownSeverity case bs >= 9.0: return vulnerability.CriticalSeverity case bs >= 7.0: return vulnerability.HighSeverity case bs >= 4.0: return vulnerability.MediumSeverity case bs >= 0.1: return vulnerability.LowSeverity case bs > 0: return vulnerability.NegligibleSeverity } return vulnerability.UnknownSeverity } // roundScore rounds the score to the nearest tenth based on first.org rounding rules // see https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding func roundScore(score float64) float64 { intInput := int(math.Round(score * 100000)) if intInput%10000 == 0 { return float64(intInput) / 100000.0 } return (math.Floor(float64(intInput)/10000.0) + 1) / 10.0 } ================================================ FILE: internal/cvss/metrics_test.go ================================================ package cvss import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/vulnerability" ) func TestParseMetricsFromVector(t *testing.T) { tests := []struct { name string vector string expectedMetrics *vulnerability.CvssMetrics wantErr require.ErrorAssertionFunc }{ { name: "valid CVSS 2.0", vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", expectedMetrics: &vulnerability.CvssMetrics{ BaseScore: 7.5, ExploitabilityScore: ptr(10.0), ImpactScore: ptr(6.5), }, }, { name: "valid CVSS 3.0", vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", expectedMetrics: &vulnerability.CvssMetrics{ BaseScore: 9.8, ExploitabilityScore: ptr(3.9), ImpactScore: ptr(5.9), }, }, { name: "valid CVSS 3.1", vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", expectedMetrics: &vulnerability.CvssMetrics{ BaseScore: 9.8, ExploitabilityScore: ptr(3.9), ImpactScore: ptr(5.9), }, }, { name: "valid CVSS 4.0", vector: "CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:H/VA:L/SC:L/SI:H/SA:L/MAC:L/MAT:P/MPR:N/S:N/R:A/RE:L/U:Clear", expectedMetrics: &vulnerability.CvssMetrics{ BaseScore: 9.1, }, }, { name: "invalid CVSS 2.0", vector: "AV:N/AC:INVALID", wantErr: require.Error, }, { name: "invalid CVSS 3.0", vector: "CVSS:3.0/AV:INVALID", wantErr: require.Error, }, { name: "invalid CVSS 3.1", vector: "CVSS:3.1/AV:INVALID", wantErr: require.Error, }, { name: "invalid CVSS 4.0", vector: "CVSS:4.0/AV:INVALID", wantErr: require.Error, }, { name: "empty vector", vector: "", wantErr: require.Error, }, { name: "malformed vector", vector: "INVALID:VECTOR", wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } result, err := ParseMetricsFromVector(tt.vector) tt.wantErr(t, err) if err != nil { assert.Nil(t, result) return } require.NotNil(t, result) assert.Equal(t, tt.expectedMetrics.BaseScore, result.BaseScore, "given vector: %s", tt.vector) if tt.expectedMetrics.ExploitabilityScore != nil { require.NotNil(t, result.ExploitabilityScore) assert.Equal(t, *tt.expectedMetrics.ExploitabilityScore, *result.ExploitabilityScore, "given vector: %s", tt.vector) } if tt.expectedMetrics.ImpactScore != nil { require.NotNil(t, result.ImpactScore) assert.Equal(t, *tt.expectedMetrics.ImpactScore, *result.ImpactScore, "given vector: %s", tt.vector) } }) } } func TestSeverityFromBaseScore(t *testing.T) { tests := []struct { name string score float64 expected vulnerability.Severity }{ { name: "unknown severity (exactly 10.0)", score: 10.0, expected: vulnerability.UnknownSeverity, }, { name: "unknown severity (greater than 10.0)", score: 10.1, expected: vulnerability.UnknownSeverity, }, { name: "critical severity (lower bound)", score: 9.0, expected: vulnerability.CriticalSeverity, }, { name: "critical severity (upper bound)", score: 9.9, expected: vulnerability.CriticalSeverity, }, { name: "high severity (lower bound)", score: 7.0, expected: vulnerability.HighSeverity, }, { name: "high severity (upper bound)", score: 8.9, expected: vulnerability.HighSeverity, }, { name: "medium severity (lower bound)", score: 4.0, expected: vulnerability.MediumSeverity, }, { name: "medium severity (upper bound)", score: 6.9, expected: vulnerability.MediumSeverity, }, { name: "low severity (lower bound)", score: 0.1, expected: vulnerability.LowSeverity, }, { name: "low severity (upper bound)", score: 3.9, expected: vulnerability.LowSeverity, }, { name: "negligible severity (between 0 and 0.1)", score: 0.05, expected: vulnerability.NegligibleSeverity, }, { name: "unknown severity (exactly zero)", score: 0.0, expected: vulnerability.UnknownSeverity, }, { name: "unknown severity (negative)", score: -1.0, expected: vulnerability.UnknownSeverity, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, SeverityFromBaseScore(tt.score)) }) } } func ptr(f float64) *float64 { return &f } ================================================ FILE: internal/dbtest/default_vulnerabilities.go ================================================ package dbtest import ( "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft/cpe" ) func DefaultVulnerabilities() []vulnerability.Vulnerability { return []vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-2024-1234", Namespace: "nvd:cpe", }, PackageName: "asdf", Constraint: version.MustGetConstraint("< 1.4", version.ApkFormat), PackageQualifiers: nil, CPEs: []cpe.CPE{ cpe.Must("cpe:2.3:*:stuff:asdf:*:*:*:*:*:*:*:*", cpe.DeclaredSource), }, Fix: vulnerability.Fix{ Versions: []string{"1.4.0"}, State: vulnerability.FixStateFixed, }, Advisories: []vulnerability.Advisory{}, RelatedVulnerabilities: nil, }, } } ================================================ FILE: internal/dbtest/server.go ================================================ package dbtest import ( "archive/tar" "bytes" "crypto/sha256" "database/sql" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/mholt/archives" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/v5/namespace" distroNs "github.com/anchore/grype/grype/db/v5/namespace/distro" "github.com/anchore/grype/grype/db/v5/namespace/language" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/file" "github.com/anchore/grype/internal/schemaver" ) type ServerBuilder struct { t *testing.T dbContents []byte DBFormat string DBBuildTime time.Time DBVersion schemaver.SchemaVer Vulnerabilities []vulnerability.Vulnerability LatestDoc *distribution.LatestDocument ServerSubdir string LatestDocFile string RequestHandler http.HandlerFunc } func (s *ServerBuilder) SetDBBuilt(t time.Time) *ServerBuilder { s.DBBuildTime = t return s } func (s *ServerBuilder) SetDBVersion(major, minor, patch int) *ServerBuilder { s.DBVersion = schemaver.New(major, minor, patch) return s } func (s *ServerBuilder) WithHandler(handler http.HandlerFunc) *ServerBuilder { s.RequestHandler = handler return s } // NewServer creates a new test db server building a single database from the provided // vulnerabilities, along with a latest.json pointing to it, optionally with any properties // specified in the provided latest parameter func NewServer(t *testing.T) *ServerBuilder { t.Helper() return &ServerBuilder{ t: t, DBFormat: "tar.zst", DBBuildTime: time.Now(), DBVersion: schemaver.New(6, 0, 0), ServerSubdir: "databases/v6", LatestDocFile: "latest.json", Vulnerabilities: DefaultVulnerabilities(), LatestDoc: &distribution.LatestDocument{ Status: "active", Archive: distribution.Archive{ Description: v6.Description{}, }, }, } } // Start starts builds a database and starts a server with the current settings // if you need to rebuild a DB or modify the behavior, you can either set // a custom RequestHandler func or modify the settings and call Start() again. // Returns a URL to the latest.json file, e.g. http://127.0.0.1:5678/v6/latest.json func (s *ServerBuilder) Start() (url string) { s.t.Helper() serverSubdir := s.ServerSubdir if serverSubdir != "" { serverSubdir += "/" } contents := s.buildDB() s.dbContents = pack(s.t, s.DBFormat, contents) handler := http.NewServeMux() handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if s.RequestHandler != nil { rw := wrappedWriter{writer: w} s.RequestHandler(&rw, r) if rw.handled { return } } dbName := "vulnerability-db_v" + s.DBVersion.String() archivePath := dbName + "." + s.DBFormat switch r.RequestURI[1:] { case serverSubdir + s.LatestDocFile: latestDoc := *s.LatestDoc latestDoc.Built.Time = s.DBBuildTime latestDoc.SchemaVersion = s.DBVersion latestDoc.Built.Time = s.DBBuildTime latestDoc.Path = archivePath latestDoc.Checksum = sha(s.dbContents) w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(latestDoc) case serverSubdir + archivePath: w.WriteHeader(http.StatusOK) _, _ = w.Write(s.dbContents) default: http.NotFound(w, r) return } }) mockSrv := httptest.NewServer(handler) s.t.Cleanup(func() { mockSrv.Close() }) return mockSrv.URL + "/" + serverSubdir + s.LatestDocFile } func sha(contents []byte) string { digest, err := file.HashReader(bytes.NewReader(contents), sha256.New()) if err != nil { panic(err) } return "sha256:" + digest } //nolint:funlen func (s *ServerBuilder) buildDB() []byte { s.t.Helper() tmp := s.t.TempDir() w, err := v6.NewWriter(v6.Config{ DBDirPath: tmp, }) require.NoError(s.t, err) aWeekAgo := time.Now().Add(-7 * 24 * time.Hour) twoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour) for _, v := range s.Vulnerabilities { prov := &v6.Provider{ ID: "nvd", Version: "1", DateCaptured: &s.DBBuildTime, } var operatingSystem *v6.OperatingSystem packageType := "" ns, err := namespace.FromString(v.Namespace) require.NoError(s.t, err) d, _ := ns.(*distroNs.Namespace) if d != nil { packageType = string(d.DistroType()) operatingSystem = &v6.OperatingSystem{ Name: d.Provider(), MajorVersion: strings.Split(d.Version(), ".")[0], } prov.ID = d.Provider() } lang, _ := ns.(*language.Namespace) if lang != nil { packageType = string(lang.Language()) } prov.Processor = prov.ID + "-processor" prov.InputDigest = sha([]byte(prov.ID)) vuln := &v6.VulnerabilityHandle{ ID: 0, Name: v.ID, Status: "", PublishedDate: &twoWeeksAgo, ModifiedDate: &aWeekAgo, WithdrawnDate: nil, ProviderID: prov.ID, Provider: prov, BlobID: 0, BlobValue: &v6.VulnerabilityBlob{ ID: v.ID, Assigners: []string{v.ID + "-assigner-1", v.ID + "-assigner-2"}, Description: v.ID + "-description", References: []v6.Reference{ { URL: "http://somewhere/" + v.ID, Tags: []string{v.ID + "-tag-1", v.ID + "-tag-2"}, }, }, //Aliases: []string{"GHSA-" + v.ID}, Severities: []v6.Severity{ { Scheme: v6.SeveritySchemeCVSS, Value: "high", Source: "", Rank: 0, }, }, }, } err = w.AddVulnerabilities(vuln) require.NoError(s.t, err) var cpes []v6.Cpe for _, cp := range v.CPEs { require.NoError(s.t, err) cpes = append(cpes, v6.Cpe{ Part: cp.Attributes.Part, Vendor: cp.Attributes.Vendor, Product: cp.Attributes.Product, Edition: cp.Attributes.Edition, Language: cp.Attributes.Language, SoftwareEdition: cp.Attributes.SWEdition, TargetHardware: cp.Attributes.TargetHW, TargetSoftware: cp.Attributes.TargetSW, Other: cp.Attributes.Other, }) } pkg := &v6.Package{ ID: 0, Ecosystem: packageType, Name: v.PackageName, } if prov.ID != "nvd" { pkg.CPEs = cpes } else { for _, c := range cpes { ac := &v6.AffectedCPEHandle{ Vulnerability: vuln, CPE: &c, BlobValue: &v6.PackageBlob{ Ranges: []v6.Range{ { Version: toAffectedVersion(v.Constraint), }, }, }, } err = w.AddAffectedCPEs(ac) require.NoError(s.t, err) } } ap := &v6.AffectedPackageHandle{ ID: 0, VulnerabilityID: 0, Vulnerability: vuln, OperatingSystemID: nil, OperatingSystem: operatingSystem, PackageID: 0, Package: pkg, BlobID: 0, BlobValue: &v6.PackageBlob{ CVEs: nil, Qualifiers: nil, Ranges: []v6.Range{ { Fix: nil, Version: toAffectedVersion(v.Constraint), }, }, }, } err = w.AddAffectedPackages(ap) require.NoError(s.t, err) } err = w.SetDBMetadata() require.NoError(s.t, err) err = w.Close() require.NoError(s.t, err) dbFile := filepath.Join(tmp, "vulnerability.db") db, err := sql.Open("sqlite", dbFile) require.NoError(s.t, err) model := s.DBVersion.Model revision := s.DBVersion.Revision addition := s.DBVersion.Addition _, err = db.Exec("update db_metadata set build_timestamp = ?, model = ?, revision = ?, addition = ?", s.DBBuildTime, model, revision, addition) require.NoError(s.t, err) err = db.Close() require.NoError(s.t, err) contents, err := os.ReadFile(dbFile) require.NoError(s.t, err) return contents } func pack(t *testing.T, typ string, contents []byte) []byte { if typ == "tar.zst" { now := time.Now() tarContents := bytes.Buffer{} tw := tar.NewWriter(&tarContents) err := tw.WriteHeader(&tar.Header{ Typeflag: tar.TypeReg, Name: "vulnerability.db", Size: int64(len(contents)), Mode: 0777, ModTime: now, }) require.NoError(t, err) _, err = tw.Write(contents) require.NoError(t, err) err = tw.Close() require.NoError(t, err) tarZstd := bytes.Buffer{} compressor, err := archives.Zstd{}.OpenWriter(&tarZstd) require.NoError(t, err) _, err = io.Copy(compressor, &tarContents) require.NoError(t, err) err = compressor.Close() require.NoError(t, err) return tarZstd.Bytes() } panic("unsupported type: " + typ) } func toAffectedVersion(c version.Constraint) v6.Version { parts := strings.SplitN(c.String(), "(", 2) if len(parts) < 2 { return v6.Version{ Constraint: strings.TrimSpace(parts[0]), } } return v6.Version{ Type: strings.TrimSpace(strings.Split(parts[1], ")")[0]), Constraint: strings.TrimSpace(parts[0]), } } type wrappedWriter struct { writer http.ResponseWriter handled bool } func (w *wrappedWriter) Header() http.Header { w.handled = true return w.writer.Header() } func (w *wrappedWriter) Write(contents []byte) (int, error) { w.handled = true return w.writer.Write(contents) } func (w *wrappedWriter) WriteHeader(statusCode int) { w.handled = true w.writer.WriteHeader(statusCode) } ================================================ FILE: internal/dbtest/server_test.go ================================================ package dbtest_test import ( "bytes" "encoding/json" "io" "net/http" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/internal/dbtest" ) func Test_NewServer(t *testing.T) { tests := []struct { name string useDefault bool serverSubdir string }{ { name: "default path", useDefault: true, }, { name: "v6 path", serverSubdir: "v6", }, { name: "root path", serverSubdir: "", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { srv := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24 * time.Hour)) if !test.useDefault { srv.ServerSubdir = test.serverSubdir } url := srv.Start() // one day ago parts := strings.Split(url, "/") urlPrefix := strings.Join(parts[:len(parts)-1], "/") get := func(url string) (status int, contents []byte, readError error) { resp, err := http.Get(url) if resp.Body != nil { defer func() { require.NoError(t, resp.Body.Close()) }() } require.NoError(t, err) buf := bytes.Buffer{} _, err = io.Copy(&buf, resp.Body) return resp.StatusCode, buf.Bytes(), err } status, content, err := get(urlPrefix + "/latest.json") require.NoError(t, err) require.Equal(t, http.StatusOK, status) // should have a latest document at the given URL var latest distribution.LatestDocument require.NoError(t, json.Unmarshal(content, &latest)) relativeDb := latest.Archive.Path require.NotEmpty(t, relativeDb) // should have a db at the relative url in the latest doc status, content, err = get(urlPrefix + "/" + relativeDb) require.NoError(t, err) require.Equal(t, http.StatusOK, status) require.NotEmpty(t, content) // should have 404 at wrong URL status, _, _ = get(urlPrefix + "/asdf") require.Equal(t, http.StatusNotFound, status) }) } } ================================================ FILE: internal/file/copy.go ================================================ package file import ( "fmt" "io" "os" "path" "github.com/spf13/afero" ) func CopyDir(fs afero.Fs, src string, dst string) error { var err error var fds []os.FileInfo // <-- afero.ReadDir returns []os.FileInfo var srcinfo os.FileInfo if srcinfo, err = fs.Stat(src); err != nil { return err } if err = fs.MkdirAll(dst, srcinfo.Mode()); err != nil { return err } if fds, err = afero.ReadDir(fs, src); err != nil { return err } for _, fd := range fds { srcPath := path.Join(src, fd.Name()) dstPath := path.Join(dst, fd.Name()) if fd.IsDir() { if err = CopyDir(fs, srcPath, dstPath); err != nil { return fmt.Errorf("could not copy dir (%s -> %s): %w", srcPath, dstPath, err) } } else { if err = CopyFile(fs, srcPath, dstPath); err != nil { return fmt.Errorf("could not copy file (%s -> %s): %w", srcPath, dstPath, err) } } } return nil } func CopyFile(fs afero.Fs, src, dst string) error { var err error var srcFd afero.File var dstFd afero.File var srcinfo os.FileInfo if srcFd, err = fs.Open(src); err != nil { return err } defer srcFd.Close() if dstFd, err = fs.Create(dst); err != nil { return err } defer dstFd.Close() if _, err = io.Copy(dstFd, srcFd); err != nil { return err } if srcinfo, err = fs.Stat(src); err != nil { return err } return fs.Chmod(dst, srcinfo.Mode()) } ================================================ FILE: internal/file/exists.go ================================================ package file import ( "os" "github.com/spf13/afero" ) func Exists(fs afero.Fs, path string) (bool, error) { info, err := fs.Stat(path) if os.IsNotExist(err) { return false, nil } else if err != nil { return false, err } return !info.IsDir(), nil } ================================================ FILE: internal/file/getter.go ================================================ package file import ( "fmt" "io" "net/http" "github.com/hashicorp/go-getter" "github.com/hashicorp/go-getter/helper/url" "github.com/spf13/afero" "github.com/wagoodman/go-progress" "github.com/anchore/clio" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/stereoscope/pkg/file" ) var ( archiveExtensions = getterDecompressorNames() ErrNonArchiveSource = fmt.Errorf("non-archive sources are not supported for directory destinations") ) type Getter interface { // GetFile downloads the give URL into the given path. The URL must reference a single file. GetFile(dst, src string, monitor ...*progress.Manual) error // GetToDir downloads the resource found at the `src` URL into the given `dst` directory. // The directory must already exist, and the remote resource MUST BE AN ARCHIVE (e.g. `.tar.gz`). GetToDir(dst, src string, monitor ...*progress.Manual) error } type HashiGoGetter struct { httpGetter getter.HttpGetter } // NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided, // it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used. func NewGetter(id clio.Identification, httpClient *http.Client) *HashiGoGetter { return &HashiGoGetter{ httpGetter: getter.HttpGetter{ Client: httpClient, Header: http.Header{ "User-Agent": []string{fmt.Sprintf("%v %v", id.Name, id.Version)}, }, }, } } func (g HashiGoGetter) GetFile(dst, src string, monitors ...*progress.Manual) error { if len(monitors) > 1 { return fmt.Errorf("multiple monitors provided, which is not allowed") } return getterClient(dst, src, false, g.httpGetter, monitors).Get() } func (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) error { // though there are multiple getters, only the http/https getter requires extra validation if err := validateHTTPSource(src); err != nil { return err } if len(monitors) > 1 { return fmt.Errorf("multiple monitors provided, which is not allowed") } return getterClient(dst, src, true, g.httpGetter, monitors).Get() } func validateHTTPSource(src string) error { // we are ignoring any sources that are not destined to use the http getter object if !stringutil.HasAnyOfPrefixes(src, "http://", "https://") { return nil } u, err := url.Parse(src) if err != nil { return fmt.Errorf("bad URL provided %q: %w", src, err) } // only allow for sources with archive extensions if !stringutil.HasAnyOfSuffixes(u.Path, archiveExtensions...) { return ErrNonArchiveSource } return nil } func getterClient(dst, src string, dir bool, httpGetter getter.HttpGetter, monitors []*progress.Manual) *getter.Client { client := &getter.Client{ Src: src, Dst: dst, Dir: dir, Getters: map[string]getter.Getter{ "http": &httpGetter, "https": &httpGetter, // note: these are the default getters from https://github.com/hashicorp/go-getter/blob/v1.5.9/get.go#L68-L74 // it is possible that other implementations need to account for custom httpclient injection, however, // that has not been accounted for at this time. "file": new(getter.FileGetter), "git": new(getter.GitGetter), "gcs": new(getter.GCSGetter), "hg": new(getter.HgGetter), "s3": new(getter.S3Getter), }, Options: mapToGetterClientOptions(monitors), } return client } func withProgress(monitor *progress.Manual) func(client *getter.Client) error { return getter.WithProgress( &progressAdapter{monitor: monitor}, ) } func mapToGetterClientOptions(monitors []*progress.Manual) []getter.ClientOption { var result []getter.ClientOption for _, monitor := range monitors { result = append(result, withProgress(monitor)) } // derived from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress.go#L23-L63 fileSizeLimit := int64(5 * file.GB) dec := getter.LimitedDecompressors(0, fileSizeLimit) fs := afero.NewOsFs() xzd := &xzDecompressor{ FileSizeLimit: fileSizeLimit, Fs: fs, } txzd := &tarXzDecompressor{ FilesLimit: 0, FileSizeLimit: fileSizeLimit, Fs: fs, } dec["xz"] = xzd dec["tar.xz"] = txzd dec["txz"] = txzd result = append(result, getter.WithDecompressors(dec)) return result } type readCloser struct { progress.Reader } func (c *readCloser) Close() error { return nil } type progressAdapter struct { monitor *progress.Manual } func (a *progressAdapter) TrackProgress(_ string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { a.monitor.Set(currentSize) a.monitor.SetTotal(totalSize) return &readCloser{ Reader: *progress.NewProxyReader(stream, a.monitor), } } func getterDecompressorNames() (names []string) { for name := range getter.Decompressors { names = append(names, name) } return names } ================================================ FILE: internal/file/getter_test.go ================================================ package file import ( "archive/tar" "bytes" "context" "crypto/x509" "fmt" "net" "net/http" "net/http/httptest" "net/url" "path" "testing" "github.com/stretchr/testify/assert" "github.com/anchore/clio" ) func TestGetter_GetFile(t *testing.T) { testCases := []struct { name string prepareClient func(*http.Client) assert assert.ErrorAssertionFunc }{ { name: "client trusts server's CA", assert: assert.NoError, }, { name: "client doesn't trust server's CA", prepareClient: removeTrustedCAs, assert: assertUnknownAuthorityError, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { requestPath := "/foo" server := newTestServer(t, withResponseForPath(t, requestPath, testFileContent)) t.Cleanup(server.Close) httpClient := getClient(t, server) if tc.prepareClient != nil { tc.prepareClient(httpClient) } getter := NewGetter(testID, httpClient) requestURL := createRequestURL(t, server, requestPath) tempDir := t.TempDir() tempFile := path.Join(tempDir, "some-destination-file") err := getter.GetFile(tempFile, requestURL) tc.assert(t, err) }) } } func TestGetter_GetToDir_FilterNonArchivesWired(t *testing.T) { testCases := []struct { name string source string assert assert.ErrorAssertionFunc }{ { name: "error out on non-archive sources", source: "http://localhost/something.txt", assert: assertErrNonArchiveSource, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { test.assert(t, NewGetter(testID, nil).GetToDir(t.TempDir(), test.source)) }) } } func TestGetter_validateHttpSource(t *testing.T) { testCases := []struct { name string source string assert assert.ErrorAssertionFunc }{ { name: "error out on non-archive sources", source: "http://localhost/something.txt", assert: assertErrNonArchiveSource, }, { name: "filter out non-archive sources with get param", source: "https://localhost/vulnerability-db_v3_2021-11-21T08:15:44Z.txt?checksum=sha256%3Ac402d01fa909a3fa85a5c6733ef27a3a51a9105b6c62b9152adbd24c08358911", assert: assertErrNonArchiveSource, }, { name: "ignore non http-https input", source: "s3://bucket/something.txt", assert: assert.NoError, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { test.assert(t, validateHTTPSource(test.source)) }) } } func TestGetter_GetToDir_CertConcerns(t *testing.T) { testCases := []struct { name string prepareClient func(*http.Client) assert assert.ErrorAssertionFunc }{ { name: "client trusts server's CA", assert: assert.NoError, }, { name: "client doesn't trust server's CA", prepareClient: removeTrustedCAs, assert: assertUnknownAuthorityError, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { requestPath := "/foo.tar" tarball := createTarball("foo", testFileContent) server := newTestServer(t, withResponseForPath(t, requestPath, tarball)) t.Cleanup(server.Close) httpClient := getClient(t, server) if tc.prepareClient != nil { tc.prepareClient(httpClient) } getter := NewGetter(testID, httpClient) requestURL := createRequestURL(t, server, requestPath) tempDir := t.TempDir() err := getter.GetToDir(tempDir, requestURL) tc.assert(t, err) }) } } func assertUnknownAuthorityError(t assert.TestingT, err error, _ ...interface{}) bool { return assert.ErrorAs(t, err, &x509.UnknownAuthorityError{}) } func assertErrNonArchiveSource(t assert.TestingT, err error, _ ...interface{}) bool { return assert.ErrorIs(t, err, ErrNonArchiveSource) } func removeTrustedCAs(client *http.Client) { client.Transport.(*http.Transport).TLSClientConfig.RootCAs = x509.NewCertPool() } // createTarball makes a single-file tarball and returns it as a byte slice. func createTarball(filename string, content []byte) []byte { tarBuffer := new(bytes.Buffer) tarWriter := tar.NewWriter(tarBuffer) tarWriter.WriteHeader(&tar.Header{ Name: filename, Size: int64(len(content)), Mode: 0600, }) tarWriter.Write(content) tarWriter.Close() return tarBuffer.Bytes() } type muxOption func(mux *http.ServeMux) func withResponseForPath(t *testing.T, path string, response []byte) muxOption { t.Helper() return func(mux *http.ServeMux) { mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) { t.Logf("server handling request: %s %s", req.Method, req.URL) _, err := w.Write(response) if err != nil { t.Fatal(err) } }) } } var testID = clio.Identification{ Name: "test-app", Version: "v0.5.3", } func newTestServer(t *testing.T, muxOptions ...muxOption) *httptest.Server { t.Helper() mux := http.NewServeMux() for _, option := range muxOptions { option(mux) } server := httptest.NewTLSServer(mux) t.Logf("new TLS server listening at %s", getHost(t, server)) return server } func createRequestURL(t *testing.T, server *httptest.Server, path string) string { t.Helper() // TODO: Figure out how to get this value from the server without hardcoding it here const testServerCertificateName = "example.com" serverURL, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } // Set URL hostname to value from TLS certificate serverURL.Host = fmt.Sprintf("%s:%s", testServerCertificateName, serverURL.Port()) serverURL.Path = path return serverURL.String() } // getClient returns an http.Client that can be used to contact the test TLS server. func getClient(t *testing.T, server *httptest.Server) *http.Client { t.Helper() httpClient := server.Client() transport := httpClient.Transport.(*http.Transport) serverHost := getHost(t, server) transport.DialContext = func(_ context.Context, _, addr string) (net.Conn, error) { t.Logf("client dialing %q for host %q", serverHost, addr) // Ensure the client dials our test server return net.Dial("tcp", serverHost) } return httpClient } // getHost extracts the host value from a server URL string. // e.g. given a server with URL "http://1.2.3.4:5000/foo", getHost returns "1.2.3.4:5000" func getHost(t *testing.T, server *httptest.Server) string { t.Helper() u, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } return u.Hostname() + ":" + u.Port() } var testFileContent = []byte("This is the content of a test file!\n") ================================================ FILE: internal/file/hasher.go ================================================ package file import ( "crypto/sha256" "encoding/hex" "fmt" "hash" "io" "strings" "github.com/OneOfOne/xxhash" "github.com/spf13/afero" ) func ValidateByHash(fs afero.Fs, path, hashStr string) (bool, string, error) { var hasher hash.Hash var hashFn string switch { case strings.HasPrefix(hashStr, "sha256:"): hashFn = "sha256" hasher = sha256.New() case strings.HasPrefix(hashStr, "xxh64:"): hashFn = "xxh64" hasher = xxhash.New64() default: return false, "", fmt.Errorf("hasher not supported or specified (given: %s)", hashStr) } hashNoPrefix := strings.Split(hashStr, ":")[1] actualHash, err := HashFile(fs, path, hasher) if err != nil { return false, "", err } return actualHash == hashNoPrefix, hashFn + ":" + actualHash, nil } func HashFile(fs afero.Fs, path string, hasher hash.Hash) (string, error) { f, err := fs.Open(path) if err != nil { return "", fmt.Errorf("failed to open file '%s': %w", path, err) } defer f.Close() return HashReader(f, hasher) } func HashReader(reader io.Reader, hasher hash.Hash) (string, error) { if _, err := io.Copy(hasher, reader); err != nil { return "", fmt.Errorf("failed to hash reader: %w", err) } return hex.EncodeToString(hasher.Sum(nil)), nil } ================================================ FILE: internal/file/hasher_test.go ================================================ package file import ( "fmt" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" ) func TestValidateByHash(t *testing.T) { testsCases := []struct { name, path, hashStr, actualHash string setup func(fs afero.Fs) valid bool err bool errMsg error }{ { name: "Valid SHA256 hash", path: "test.txt", hashStr: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", setup: func(fs afero.Fs) { afero.WriteFile(fs, "test.txt", []byte("test"), 0644) }, actualHash: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", valid: true, err: false, }, { name: "Invalid SHA256 hash", path: "test.txt", hashStr: "sha256:deadbeef", setup: func(fs afero.Fs) { afero.WriteFile(fs, "test.txt", []byte("test"), 0644) }, actualHash: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", valid: false, err: false, }, { name: "Unsupported hash function", path: "test.txt", hashStr: "md5:deadbeef", setup: func(fs afero.Fs) { afero.WriteFile(fs, "test.txt", []byte("test"), 0644) }, actualHash: "", valid: false, err: true, errMsg: fmt.Errorf("hasher not supported or specified (given: md5:deadbeef)"), }, { name: "File does not exist", path: "nonexistent.txt", hashStr: "sha256:deadbeef", setup: func(fs afero.Fs) {}, valid: false, actualHash: "", err: true, }, } for _, tc := range testsCases { t.Run(tc.name, func(t *testing.T) { fs := afero.NewMemMapFs() tc.setup(fs) valid, actualHash, err := ValidateByHash(fs, tc.path, tc.hashStr) assert.Equal(t, tc.valid, valid) assert.Equal(t, tc.actualHash, actualHash) if tc.err { assert.Error(t, err) } else { assert.NoError(t, err) } if tc.errMsg != nil { assert.Equal(t, tc.errMsg, err) } }) } } ================================================ FILE: internal/file/tar_xz_decompressor.go ================================================ package file import ( "archive/tar" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/spf13/afero" "github.com/xi2/xz" ) // Note: this is a copy of the TarXzDecompressor from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress_txz.go // with the xz lib swapped out (for performance). A few adjustments were made: // - refactored to use afero filesystem abstraction // - fixed some linting issues // TarXzDecompressor is an implementation of Decompressor that can // decompress tar.xz files. type tarXzDecompressor struct { // FileSizeLimit limits the total size of all // decompressed files. // // The zero value means no limit. FileSizeLimit int64 // FilesLimit limits the number of files that are // allowed to be decompressed. // // The zero value means no limit. FilesLimit int Fs afero.Fs } func (d *tarXzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error { // If we're going into a directory we should make that first mkdir := dst if !dir { mkdir = filepath.Dir(dst) } if err := d.Fs.MkdirAll(mkdir, mode(0755, umask)); err != nil { return err } // File first f, err := d.Fs.Open(src) if err != nil { return err } defer f.Close() // xz compression is second txzR, err := xz.NewReader(f, 0) if err != nil { return fmt.Errorf("error opening an xz reader for %s: %s", src, err) } return untar(d.Fs, txzR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit) } // untar is a shared helper for untarring an archive. The reader should provide // an uncompressed view of the tar archive. func untar(fs afero.Fs, input io.Reader, dst, src string, dir bool, umask os.FileMode, fileSizeLimit int64, filesLimit int) error { // nolint:funlen,gocognit tarR := tar.NewReader(input) done := false dirHdrs := []*tar.Header{} now := time.Now() var ( fileSize int64 filesCount int ) for { if filesLimit > 0 { filesCount++ if filesCount > filesLimit { return fmt.Errorf("tar archive contains too many files: %d > %d", filesCount, filesLimit) } } hdr, err := tarR.Next() if err == io.EOF { if !done { // Empty archive return fmt.Errorf("empty archive: %s", src) } break } if err != nil { return err } switch hdr.Typeflag { case tar.TypeSymlink, tar.TypeLink: // to prevent any potential indirect traversal attacks continue case tar.TypeXGlobalHeader, tar.TypeXHeader: // don't unpack extended headers as files continue } path := dst if dir { // Disallow parent traversal if containsDotDot(hdr.Name) { return fmt.Errorf("entry contains '..': %s", hdr.Name) } path = filepath.Join(path, hdr.Name) // nolint:gosec // hdr.Name is checked above } fileInfo := hdr.FileInfo() fileSize += fileInfo.Size() if fileSizeLimit > 0 && fileSize > fileSizeLimit { return fmt.Errorf("tar archive larger than limit: %d", fileSizeLimit) } if fileInfo.IsDir() { if !dir { return fmt.Errorf("expected a single file: %s", src) } // A directory, just make the directory and continue unarchiving... if err := fs.MkdirAll(path, mode(0755, umask)); err != nil { return err } // Record the directory information so that we may set its attributes // after all files have been extracted dirHdrs = append(dirHdrs, hdr) continue } // There is no ordering guarantee that a file in a directory is // listed before the directory dstPath := filepath.Dir(path) // Check that the directory exists, otherwise create it if _, err := fs.Stat(dstPath); os.IsNotExist(err) { if err := fs.MkdirAll(dstPath, mode(0755, umask)); err != nil { return err } } // We have a file. If we already decoded, then it is an error if !dir && done { return fmt.Errorf("expected a single file, got multiple: %s", src) } // Mark that we're done so future in single file mode errors done = true // Size limit is tracked using the returned file info. err = copyReader(fs, path, tarR, hdr.FileInfo().Mode(), umask, 0) if err != nil { return err } // Set the access and modification time if valid, otherwise default to current time aTime := now mTime := now if hdr.AccessTime.Unix() > 0 { aTime = hdr.AccessTime } if hdr.ModTime.Unix() > 0 { mTime = hdr.ModTime } if err := fs.Chtimes(path, aTime, mTime); err != nil { return err } } // Perform a final pass over extracted directories to update metadata for _, dirHdr := range dirHdrs { path := filepath.Join(dst, dirHdr.Name) // nolint:gosec // hdr.Name is checked above // Chmod the directory since they might be created before we know the mode flags if err := fs.Chmod(path, mode(dirHdr.FileInfo().Mode(), umask)); err != nil { return err } // Set the mtime/atime attributes since they would have been changed during extraction aTime := now mTime := now if dirHdr.AccessTime.Unix() > 0 { aTime = dirHdr.AccessTime } if dirHdr.ModTime.Unix() > 0 { mTime = dirHdr.ModTime } if err := fs.Chtimes(path, aTime, mTime); err != nil { return err } } return nil } // containsDotDot checks if the filepath value v contains a ".." entry. // This will check filepath components by splitting along / or \. This // function is copied directly from the Go net/http implementation. func containsDotDot(v string) bool { if !strings.Contains(v, "..") { return false } for _, ent := range strings.FieldsFunc(v, isSlashRune) { if ent == ".." { return true } } return false } func isSlashRune(r rune) bool { return r == '/' || r == '\\' } ================================================ FILE: internal/file/tar_xz_decompressor_test.go ================================================ package file import ( "archive/tar" "bytes" "path/filepath" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ulikunitz/xz" ) func TestTarXzDecompressor_Decompress(t *testing.T) { files := map[string]string{ "file1.txt": "This is file 1.", "file2.txt": "This is file 2.", } fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstDir := filepath.Join(tmpDir, "decompressed") decompressor := &tarXzDecompressor{ Fs: fs, } err := decompressor.Decompress(dstDir, srcFile, true, 0000) require.NoError(t, err) for name, content := range files { data, err := afero.ReadFile(fs, filepath.Join(dstDir, name)) require.NoError(t, err) assert.Equal(t, content, string(data)) } } func TestTarXzDecompressor_DecompressWithNestedDirs(t *testing.T) { files := map[string]string{ "file1.txt": "This is file 1.", "dir1/file2.txt": "This is file 2 in dir1.", "dir1/dir2/file3.txt": "This is file 3 in dir1/dir2.", "dir1/dir2/dir3/file4.txt": "This is file 4 in dir1/dir2/dir3.", } fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstDir := filepath.Join(tmpDir, "decompressed") decompressor := &tarXzDecompressor{ Fs: fs, } err := decompressor.Decompress(dstDir, srcFile, true, 0000) require.NoError(t, err) for name, content := range files { data, err := afero.ReadFile(fs, filepath.Join(dstDir, name)) require.NoError(t, err) assert.Equal(t, content, string(data)) } } func TestTarXzDecompressor_FileSizeLimit(t *testing.T) { files := map[string]string{ "file1.txt": "This is file 1.", "file2.txt": "This is file 2.", } fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstDir := filepath.Join(tmpDir, "decompressed") decompressor := &tarXzDecompressor{ FileSizeLimit: int64(10), // setting a small file size limit Fs: fs, } err := decompressor.Decompress(dstDir, srcFile, true, 0000) require.Error(t, err) assert.Contains(t, err.Error(), "tar archive larger than limit") } func TestTarXzDecompressor_FilesLimit(t *testing.T) { files := map[string]string{ "file1.txt": "This is file 1.", "file2.txt": "This is file 2.", } fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstDir := filepath.Join(tmpDir, "decompressed") decompressor := &tarXzDecompressor{ FilesLimit: 1, // setting a limit of 1 file Fs: fs, } err := decompressor.Decompress(dstDir, srcFile, true, 0000) require.Error(t, err) assert.Contains(t, err.Error(), "tar archive contains too many files") } func TestTarXzDecompressor_DecompressSingleFile(t *testing.T) { files := map[string]string{ "file1.txt": "This is file 1.", } fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstFile := filepath.Join(tmpDir, "single_file.txt") decompressor := &tarXzDecompressor{ Fs: fs, } err := decompressor.Decompress(dstFile, srcFile, false, 0000) require.NoError(t, err) data, err := afero.ReadFile(fs, dstFile) require.NoError(t, err) assert.Equal(t, files["file1.txt"], string(data)) } func TestTarXzDecompressor_EmptyArchive(t *testing.T) { files := map[string]string{} fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstDir := filepath.Join(tmpDir, "decompressed") decompressor := &tarXzDecompressor{ Fs: fs, } err := decompressor.Decompress(dstDir, srcFile, true, 0000) require.Error(t, err) assert.Contains(t, err.Error(), "empty archive") } func TestTarXzDecompressor_PathTraversal(t *testing.T) { files := map[string]string{ "../traversal_file.txt": "This file should not be extracted.", } fs := afero.NewMemMapFs() srcFile, tmpDir := createTarXzFromFiles(t, fs, files) dstDir := filepath.Join(tmpDir, "decompressed") decompressor := &tarXzDecompressor{ Fs: fs, } err := decompressor.Decompress(dstDir, srcFile, true, 0000) require.Error(t, err) assert.Contains(t, err.Error(), "entry contains '..'") } func createTarXzFromFiles(t *testing.T, fs afero.Fs, files map[string]string) (string, string) { t.Helper() tmpDir, err := afero.TempDir(fs, "", "tar_xz_decompressor_test") require.NoError(t, err) srcFile := filepath.Join(tmpDir, "src_file.tar.xz") var buf bytes.Buffer xzWriter, err := xz.NewWriter(&buf) require.NoError(t, err) tarWriter := tar.NewWriter(xzWriter) for name, content := range files { dir := filepath.Dir(name) if dir != "." { hdr := &tar.Header{ Name: dir + "/", Mode: 0755, Typeflag: tar.TypeDir, } err := tarWriter.WriteHeader(hdr) require.NoError(t, err) } hdr := &tar.Header{ Name: name, Mode: 0600, Size: int64(len(content)), } err := tarWriter.WriteHeader(hdr) require.NoError(t, err) _, err = tarWriter.Write([]byte(content)) require.NoError(t, err) } err = tarWriter.Close() require.NoError(t, err) err = xzWriter.Close() require.NoError(t, err) err = afero.WriteFile(fs, srcFile, buf.Bytes(), 0644) require.NoError(t, err) return srcFile, tmpDir } ================================================ FILE: internal/file/xz_decompressor.go ================================================ package file import ( "fmt" "io" "os" "path/filepath" "github.com/spf13/afero" "github.com/xi2/xz" ) // Note: this is a copy of the XzDecompressor from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress_xz.go // with the xz lib swapped out (for performance). A few adjustments were made: // - refactored to use afero filesystem abstraction // - fixed some linting issues // xzDecompressor is an implementation of Decompressor that can decompress xz files. type xzDecompressor struct { // FileSizeLimit limits the size of a decompressed file. // // The zero value means no limit. FileSizeLimit int64 Fs afero.Fs } func (d *xzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error { // Directory isn't supported at all if dir { return fmt.Errorf("xz-compressed files can only unarchive to a single file") } // If we're going into a directory we should make that first if err := d.Fs.MkdirAll(filepath.Dir(dst), mode(0755, umask)); err != nil { return err } // File first f, err := d.Fs.Open(src) if err != nil { return err } defer f.Close() // xz compression is second xzR, err := xz.NewReader(f, 0) if err != nil { return err } // Copy it out, potentially using a file size limit. return copyReader(d.Fs, dst, xzR, 0622, umask, d.FileSizeLimit) } // copyReader copies from an io.Reader into a file, using umask to create the dst file func copyReader(fs afero.Fs, dst string, src io.Reader, fmode, umask os.FileMode, fileSizeLimit int64) error { dstF, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fmode) if err != nil { return err } defer dstF.Close() if fileSizeLimit > 0 { src = io.LimitReader(src, fileSizeLimit) } _, err = io.Copy(dstF, src) if err != nil { return err } // Explicitly chmod; the process umask is unconditionally applied otherwise. // We'll mask the mode with our own umask, but that may be different than // the process umask return fs.Chmod(dst, mode(fmode, umask)) } // mode returns the file mode masked by the umask func mode(mode, umask os.FileMode) os.FileMode { return mode & ^umask } ================================================ FILE: internal/file/xz_decompressor_test.go ================================================ package file import ( "os" "path/filepath" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ulikunitz/xz" ) func TestXzDecompressor_Decompress(t *testing.T) { content := "This is a test for xz decompression." fs := afero.NewMemMapFs() srcFile, tmpDir := createXZFromString(t, fs, content) dstFile := filepath.Join(tmpDir, "dst_file.txt") decompressor := &xzDecompressor{ Fs: fs, } err := decompressor.Decompress(dstFile, srcFile, false, 0000) require.NoError(t, err) data, err := afero.ReadFile(fs, dstFile) require.NoError(t, err) assert.Equal(t, content, string(data)) } func TestXzDecompressor_FileSizeLimit(t *testing.T) { content := "This is a test for xz decompression with file size limit." fs := afero.NewMemMapFs() srcFile, tmpDir := createXZFromString(t, fs, content) dstFile := filepath.Join(tmpDir, "dst_file.txt") fileSizeLimit := int64(10) decompressor := &xzDecompressor{ FileSizeLimit: fileSizeLimit, Fs: fs, } err := decompressor.Decompress(dstFile, srcFile, false, 0000) require.NoError(t, err) data, err := afero.ReadFile(fs, dstFile) require.NoError(t, err) assert.Equal(t, content[:fileSizeLimit], string(data)) } func TestCopyReader(t *testing.T) { content := "This is the content for testing copyReader." fs := afero.NewMemMapFs() tmpDir := t.TempDir() srcFile := filepath.Join(tmpDir, "src_file.txt") err := afero.WriteFile(fs, srcFile, []byte(content), 0644) require.NoError(t, err) srcF, err := fs.Open(srcFile) require.NoError(t, err) defer srcF.Close() dstFile := filepath.Join(tmpDir, "dst_file.txt") err = copyReader(fs, dstFile, srcF, 0644, 0000, 0) require.NoError(t, err) info, err := fs.Stat(dstFile) require.NoError(t, err) assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) data, err := afero.ReadFile(fs, dstFile) assert.NoError(t, err) assert.Equal(t, content, string(data)) } func createXZFromString(t *testing.T, fs afero.Fs, content string) (string, string) { t.Helper() tmpDir, err := afero.TempDir(fs, "", "xz_decompressor_test") require.NoError(t, err) srcFile := filepath.Join(tmpDir, "src_file.xz") f, err := fs.Create(srcFile) require.NoError(t, err) defer f.Close() xzW, err := xz.NewWriter(f) require.NoError(t, err) defer xzW.Close() _, err = xzW.Write([]byte(content)) assert.NoError(t, err) return srcFile, tmpDir } ================================================ FILE: internal/format/format.go ================================================ package format import ( "strings" ) const ( UnknownFormat Format = "unknown" JSONFormat Format = "json" TableFormat Format = "table" CycloneDXFormat Format = "cyclonedx" CycloneDXJSON Format = "cyclonedx-json" CycloneDXXML Format = "cyclonedx-xml" SarifFormat Format = "sarif" TemplateFormat Format = "template" // DEPRECATED <-- TODO: remove in v1.0 EmbeddedVEXJSON Format = "embedded-cyclonedx-vex-json" EmbeddedVEXXML Format = "embedded-cyclonedx-vex-xml" ) // Format is a dedicated type to represent a specific kind of presenter output format. type Format string func (f Format) String() string { return string(f) } // Parse returns the presenter.format specified by the given user input. func Parse(userInput string) Format { switch strings.ToLower(userInput) { case "": return TableFormat case strings.ToLower(JSONFormat.String()): return JSONFormat case strings.ToLower(TableFormat.String()): return TableFormat case strings.ToLower(SarifFormat.String()): return SarifFormat case strings.ToLower(TemplateFormat.String()): return TemplateFormat case strings.ToLower(CycloneDXFormat.String()): return CycloneDXFormat case strings.ToLower(CycloneDXJSON.String()): return CycloneDXJSON case strings.ToLower(CycloneDXXML.String()): return CycloneDXXML case strings.ToLower(EmbeddedVEXJSON.String()): return CycloneDXJSON case strings.ToLower(EmbeddedVEXXML.String()): return CycloneDXFormat default: return UnknownFormat } } // AvailableFormats is a list of presenter format options available to users. var AvailableFormats = []Format{ JSONFormat, TableFormat, CycloneDXFormat, CycloneDXJSON, SarifFormat, TemplateFormat, } // DeprecatedFormats TODO: remove in v1.0 var DeprecatedFormats = []Format{ EmbeddedVEXJSON, EmbeddedVEXXML, } ================================================ FILE: internal/format/format_test.go ================================================ package format import ( "testing" "github.com/stretchr/testify/assert" ) func TestParse(t *testing.T) { cases := []struct { input string expected Format }{ { "", TableFormat, }, { "table", TableFormat, }, { "jSOn", JSONFormat, }, { "booboodepoopoo", UnknownFormat, }, } for _, tc := range cases { t.Run(tc.input, func(t *testing.T) { actual := Parse(tc.input) assert.Equal(t, tc.expected, actual, "unexpected result for input %q", tc.input) }) } } ================================================ FILE: internal/format/presenter.go ================================================ package format import ( "github.com/wagoodman/go-presenter" "github.com/anchore/grype/grype/presenter/cyclonedx" "github.com/anchore/grype/grype/presenter/json" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/presenter/sarif" "github.com/anchore/grype/grype/presenter/table" "github.com/anchore/grype/grype/presenter/template" "github.com/anchore/grype/internal/log" ) type PresentationConfig struct { TemplateFilePath string ShowSuppressed bool Pretty bool } // GetPresenter retrieves a Presenter that matches a CLI option func GetPresenter(format Format, c PresentationConfig, pb models.PresenterConfig) presenter.Presenter { switch format { case JSONFormat: return json.NewPresenter(pb) case TableFormat: return table.NewPresenter(pb, c.ShowSuppressed) // NOTE: cyclonedx is identical to EmbeddedVEXJSON // The cyclonedx library only provides two BOM formats: JSON and XML // These embedded formats will be removed in v1.0 case CycloneDXFormat: return cyclonedx.NewXMLPresenter(pb) case CycloneDXJSON: return cyclonedx.NewJSONPresenter(pb) case CycloneDXXML: return cyclonedx.NewXMLPresenter(pb) case SarifFormat: return sarif.NewPresenter(pb) case TemplateFormat: return template.NewPresenter(pb, c.TemplateFilePath) // DEPRECATED TODO: remove in v1.0 case EmbeddedVEXJSON: log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0") return cyclonedx.NewJSONPresenter(pb) case EmbeddedVEXXML: log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0") return cyclonedx.NewXMLPresenter(pb) default: return nil } } ================================================ FILE: internal/format/writer.go ================================================ package format import ( "bytes" "fmt" "io" "os" "path/filepath" "strings" "github.com/hashicorp/go-multierror" "github.com/anchore/go-homedir" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" ) type ScanResultWriter interface { Write(result models.PresenterConfig) error } var _ ScanResultWriter = (*scanResultMultiWriter)(nil) var _ interface { io.Closer ScanResultWriter } = (*scanResultStreamWriter)(nil) // MakeScanResultWriter creates a ScanResultWriter for output or returns an error. this will either return a valid writer // or an error but neither both and if there is no error, ScanResultWriter.Close() should be called func MakeScanResultWriter(outputs []string, defaultFile string, cfg PresentationConfig) (ScanResultWriter, error) { outputOptions, err := parseOutputFlags(outputs, defaultFile, cfg) if err != nil { return nil, err } writer, err := newMultiWriter(outputOptions...) if err != nil { return nil, err } return writer, nil } // MakeScanResultWriterForFormat creates a ScanResultWriter for the given format or returns an error. func MakeScanResultWriterForFormat(f string, path string, cfg PresentationConfig) (ScanResultWriter, error) { format := Parse(f) if format == UnknownFormat { return nil, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, f, AvailableFormats) } writer, err := newMultiWriter(newWriterDescription(format, path, cfg)) if err != nil { return nil, err } return writer, nil } // parseOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file func parseOutputFlags(outputs []string, defaultFile string, cfg PresentationConfig) (out []scanResultWriterDescription, errs error) { // always should have one option -- we generally get the default of "table", but just make sure if len(outputs) == 0 { outputs = append(outputs, TableFormat.String()) } for _, name := range outputs { name = strings.TrimSpace(name) // split to at most two parts for = parts := strings.SplitN(name, "=", 2) // the format name is the first part name = parts[0] // default to the --file or empty string if not specified file := defaultFile // If a file is specified as part of the output formatName, use that if len(parts) > 1 { file = parts[1] } format := Parse(name) if format == UnknownFormat { errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, AvailableFormats)) continue } out = append(out, newWriterDescription(format, file, cfg)) } return out, errs } // scanResultWriterDescription Format and path strings used to create ScanResultWriter type scanResultWriterDescription struct { Format Format Path string Cfg PresentationConfig } func newWriterDescription(f Format, p string, cfg PresentationConfig) scanResultWriterDescription { expandedPath, err := homedir.Expand(p) if err != nil { log.Warnf("could not expand given writer output path=%q: %w", p, err) // ignore errors expandedPath = p } return scanResultWriterDescription{ Format: f, Path: expandedPath, Cfg: cfg, } } // scanResultMultiWriter holds a list of child ScanResultWriters to apply all Write and Close operations to type scanResultMultiWriter struct { writers []ScanResultWriter } // newMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used func newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiWriter, err error) { if len(options) == 0 { return nil, fmt.Errorf("no output options provided") } out := &scanResultMultiWriter{} for _, option := range options { switch len(option.Path) { case 0: out.writers = append(out.writers, &scanResultPublisher{ format: option.Format, cfg: option.Cfg, }) default: // create any missing subdirectories dir := filepath.Dir(option.Path) if dir != "" { s, err := os.Stat(dir) if err != nil { err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ? if err != nil { return nil, err } } else if !s.IsDir() { return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path) } } fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return nil, fmt.Errorf("unable to create report file: %w", err) } out.writers = append(out.writers, &scanResultStreamWriter{ format: option.Format, out: fileOut, cfg: option.Cfg, }) } } return out, nil } // Write writes the result to all writers func (m *scanResultMultiWriter) Write(s models.PresenterConfig) (errs error) { for _, w := range m.writers { err := w.Write(s) if err != nil { errs = multierror.Append(errs, fmt.Errorf("unable to write result: %w", err)) } } return errs } // scanResultStreamWriter implements ScanResultWriter for a given format and io.Writer, also providing a close function for cleanup type scanResultStreamWriter struct { format Format cfg PresentationConfig out io.Writer } // Write the provided result to the data stream func (w *scanResultStreamWriter) Write(s models.PresenterConfig) error { pres := GetPresenter(w.format, w.cfg, s) if err := pres.Present(w.out); err != nil { return fmt.Errorf("unable to encode result: %w", err) } return nil } // Close any resources, such as open files func (w *scanResultStreamWriter) Close() error { if closer, ok := w.out.(io.Closer); ok { return closer.Close() } return nil } // scanResultPublisher implements ScanResultWriter that publishes results to the event bus type scanResultPublisher struct { format Format cfg PresentationConfig } // Write the provided result to the data stream func (w *scanResultPublisher) Write(s models.PresenterConfig) error { pres := GetPresenter(w.format, w.cfg, s) buf := &bytes.Buffer{} if err := pres.Present(buf); err != nil { return fmt.Errorf("unable to encode result: %w", err) } bus.Report(buf.String()) return nil } ================================================ FILE: internal/format/writer_test.go ================================================ package format import ( "path/filepath" "strings" "testing" "github.com/docker/docker/pkg/homedir" "github.com/stretchr/testify/assert" ) func Test_MakeScanResultWriter(t *testing.T) { tests := []struct { outputs []string wantErr assert.ErrorAssertionFunc }{ { outputs: []string{"json"}, wantErr: assert.NoError, }, { outputs: []string{"table", "json"}, wantErr: assert.NoError, }, { outputs: []string{"unknown"}, wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool { return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`) }, }, } for _, tt := range tests { _, err := MakeScanResultWriter(tt.outputs, "", PresentationConfig{}) tt.wantErr(t, err) } } func Test_newSBOMMultiWriter(t *testing.T) { type writerConfig struct { format string file string } tmp := t.TempDir() testName := func(options []scanResultWriterDescription, err bool) string { var out []string for _, opt := range options { out = append(out, string(opt.Format)+"="+opt.Path) } errs := "" if err { errs = "(err)" } return strings.Join(out, ", ") + errs } tests := []struct { outputs []scanResultWriterDescription err bool expected []writerConfig }{ { outputs: []scanResultWriterDescription{}, err: true, }, { outputs: []scanResultWriterDescription{ { Format: "table", Path: "", }, }, expected: []writerConfig{ { format: "table", }, }, }, { outputs: []scanResultWriterDescription{ { Format: "json", }, }, expected: []writerConfig{ { format: "json", }, }, }, { outputs: []scanResultWriterDescription{ { Format: "json", Path: "test-2.json", }, }, expected: []writerConfig{ { format: "json", file: "test-2.json", }, }, }, { outputs: []scanResultWriterDescription{ { Format: "json", Path: "test-3/1.json", }, { Format: "spdx-json", Path: "test-3/2.json", }, }, expected: []writerConfig{ { format: "json", file: "test-3/1.json", }, { format: "spdx-json", file: "test-3/2.json", }, }, }, { outputs: []scanResultWriterDescription{ { Format: "text", }, { Format: "spdx-json", Path: "test-4.json", }, }, expected: []writerConfig{ { format: "text", }, { format: "spdx-json", file: "test-4.json", }, }, }, } for _, test := range tests { t.Run(testName(test.outputs, test.err), func(t *testing.T) { outputs := test.outputs for i := range outputs { if outputs[i].Path != "" { outputs[i].Path = tmp + outputs[i].Path } } mw, err := newMultiWriter(outputs...) if test.err { assert.Error(t, err) return } else { assert.NoError(t, err) } assert.Len(t, mw.writers, len(test.expected)) for i, e := range test.expected { switch w := mw.writers[i].(type) { case *scanResultStreamWriter: assert.Equal(t, string(w.format), e.format) assert.NotNil(t, w.out) if e.file != "" { assert.FileExists(t, tmp+e.file) } case *scanResultPublisher: assert.Equal(t, string(w.format), e.format) default: t.Fatalf("unknown writer type: %T", w) } } }) } } func Test_newSBOMWriterDescription(t *testing.T) { tests := []struct { name string path string expected string }{ { name: "expand home dir", path: "~/place.txt", expected: filepath.Join(homedir.Get(), "place.txt"), }, { name: "passthrough other paths", path: "/other/place.txt", expected: "/other/place.txt", }, { name: "no path", path: "", expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := newWriterDescription("table", tt.path, PresentationConfig{}) assert.Equal(t, tt.expected, o.Path) }) } } ================================================ FILE: internal/input.go ================================================ package internal import ( "fmt" "os" ) // IsStdinPipeOrRedirect returns true if stdin is provided via pipe or redirect func IsStdinPipeOrRedirect() (bool, error) { fi, err := os.Stdin.Stat() if err != nil { return false, fmt.Errorf("unable to determine if there is piped input: %w", err) } // note: we should NOT use the absence of a character device here as the hint that there may be input expected // on stdin, as running grype as a subprocess you would expect no character device to be present but input can // be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine // if there *may* be bytes that will show up on stdin that should be used for the analysis source. return fi.Mode()&os.ModeNamedPipe != 0 || fi.Size() > 0, nil } ================================================ FILE: internal/log/errors.go ================================================ package log import "io" func CloseAndLogError(closer io.Closer, location string) { if closer == nil { Debug("no closer provided when attempting to close: %v", location) return } err := closer.Close() if err != nil { Debug("failed to close file: %v due to: %v", location, err) } } ================================================ FILE: internal/log/log.go ================================================ /* Package log contains the singleton object and helper functions for facilitating logging within the syft library. */ package log import ( "github.com/anchore/go-logger" "github.com/anchore/go-logger/adapter/discard" "github.com/anchore/go-logger/adapter/redact" red "github.com/anchore/grype/internal/redact" ) // log is the singleton used to facilitate logging internally within var log = discard.New() func Set(l logger.Logger) { // though the application will automatically have a redaction logger, library consumers may not be doing this. // for this reason we additionally ensure there is a redaction logger configured for any logger passed. The // source of truth for redaction values is still in the internal redact package. If the passed logger is already // redacted, then this is a no-op. store := red.Get() if store != nil { l = redact.New(l, store) } log = l } func Get() logger.Logger { return log } // Errorf takes a formatted template string and template arguments for the error logging level. func Errorf(format string, args ...interface{}) { log.Errorf(format, args...) } // Error logs the given arguments at the error logging level. func Error(args ...interface{}) { log.Error(args...) } // Warnf takes a formatted template string and template arguments for the warning logging level. func Warnf(format string, args ...interface{}) { log.Warnf(format, args...) } // Warn logs the given arguments at the warning logging level. func Warn(args ...interface{}) { log.Warn(args...) } // Infof takes a formatted template string and template arguments for the info logging level. func Infof(format string, args ...interface{}) { log.Infof(format, args...) } // Info logs the given arguments at the info logging level. func Info(args ...interface{}) { log.Info(args...) } // Debugf takes a formatted template string and template arguments for the debug logging level. func Debugf(format string, args ...interface{}) { log.Debugf(format, args...) } // Debug logs the given arguments at the debug logging level. func Debug(args ...interface{}) { log.Debug(args...) } // Tracef takes a formatted template string and template arguments for the trace logging level. func Tracef(format string, args ...interface{}) { log.Tracef(format, args...) } // Trace logs the given arguments at the trace logging level. func Trace(args ...interface{}) { log.Trace(args...) } // WithFields returns a message logger with multiple key-value fields. func WithFields(fields ...interface{}) logger.MessageLogger { return log.WithFields(fields...) } // Nested returns a new logger with hard coded key-value pairs func Nested(fields ...interface{}) logger.Logger { return log.Nested(fields...) } ================================================ FILE: internal/redact/redact.go ================================================ package redact import "github.com/anchore/go-logger/adapter/redact" var store redact.Store func Set(s redact.Store) { if store != nil { // if someone is trying to set a redaction store and we already have one then something is wrong. The store // that we're replacing might already have values in it, so we should never replace it. panic("replace existing redaction store (probably unintentional)") } store = s } func Get() redact.Store { return store } func Add(vs ...string) { if store == nil { // if someone is trying to add values that should never be output and we don't have a store then something is wrong. // we should never accidentally output values that should be redacted, thus we panic here. panic("cannot add redactions without a store") } store.Add(vs...) } func Apply(value string) string { if store == nil { // if someone is trying to add values that should never be output and we don't have a store then something is wrong. // we should never accidentally output values that should be redacted, thus we panic here. panic("cannot apply redactions without a store") } return store.RedactString(value) } ================================================ FILE: internal/regex_helpers.go ================================================ package internal import "regexp" // MatchNamedCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map. // This is only for the first match in the regex. Callers shouldn't be providing regexes with multiple capture groups with the same name. func MatchNamedCaptureGroups(regEx *regexp.Regexp, content string) map[string]string { // note: we are looking across all matches and stopping on the first non-empty match. Why? Take the following example: // input: "cool something to match against" pattern: `((?Pmatch) (?Pagainst))?`. Since the pattern is // encapsulated in an optional capture group, there will be results for each character, but the results will match // on nothing. The only "true" match will be at the end ("match against"). allMatches := regEx.FindAllStringSubmatch(content, -1) var results map[string]string for _, match := range allMatches { // fill a candidate results map with named capture group results, accepting empty values, but not groups with // no names for nameIdx, name := range regEx.SubexpNames() { if nameIdx > len(match) || len(name) == 0 { continue } if results == nil { results = make(map[string]string) } results[name] = match[nameIdx] } // note: since we are looking for the first best potential match we should stop when we find the first one // with non-empty results. if !isEmptyMap(results) { break } } return results } func isEmptyMap(m map[string]string) bool { if len(m) == 0 { return true } for _, value := range m { if value != "" { return false } } return true } ================================================ FILE: internal/regex_helpers_test.go ================================================ package internal import ( "regexp" "testing" "github.com/stretchr/testify/assert" ) func TestMatchCaptureGroups(t *testing.T) { tests := []struct { name string input string pattern string expected map[string]string }{ { name: "go-case", input: "match this thing", pattern: `(?Pmatch).*(?Pthing)`, expected: map[string]string{ "name": "match", "version": "thing", }, }, { name: "only matches the first instance", input: "match this thing batch another think", pattern: `(?P[mb]atch).*?(?Pthin[gk])`, expected: map[string]string{ "name": "match", "version": "thing", }, }, { name: "nested capture groups", input: "cool something to match against", pattern: `((?Pmatch) (?Pagainst))`, expected: map[string]string{ "name": "match", "version": "against", }, }, { name: "nested optional capture groups", input: "cool something to match against", pattern: `((?Pmatch) (?Pagainst))?`, expected: map[string]string{ "name": "match", "version": "against", }, }, { name: "nested optional capture groups with larger match", input: "cool something to match against match never", pattern: `.*?((?Pmatch) (?P(against|never)))?`, expected: map[string]string{ "name": "match", "version": "against", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := MatchNamedCaptureGroups(regexp.MustCompile(test.pattern), test.input) assert.Equal(t, test.expected, actual) }) } } ================================================ FILE: internal/schemaver/schema_ver.go ================================================ package schemaver import ( "encoding/json" "fmt" "strconv" "strings" ) type SchemaVer struct { Model int // breaking changes Revision int // potentially-breaking changes Addition int // additions only } func New(model, revision, addition int) SchemaVer { return SchemaVer{ Model: model, Revision: revision, Addition: addition, } } func Parse(s string) (SchemaVer, error) { // must provide model.revision.addition parts := strings.Split(strings.TrimSpace(s), ".") if len(parts) != 3 { return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s) } // check that all parts are integers model, err := strconv.Atoi(strings.TrimPrefix(parts[0], "v")) if err != nil || model < 1 { return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s) } revision, err := strconv.Atoi(parts[1]) if err != nil || revision < 0 { return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s) } addition, err := strconv.Atoi(parts[2]) if err != nil || addition < 0 { return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s) } return New(model, revision, addition), nil } func (s SchemaVer) Valid() bool { return s.Model > 0 && s.Revision >= 0 && s.Addition >= 0 } func (s SchemaVer) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%s"`, s.String())), nil } func (s *SchemaVer) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return fmt.Errorf("failed to unmarshal schema version as string: %w", err) } parsed, err := Parse(str) if err != nil { return fmt.Errorf("failed to parse schema version: %w", err) } *s = parsed return nil } func (s SchemaVer) String() string { return fmt.Sprintf("v%d.%d.%d", s.Model, s.Revision, s.Addition) } func (s SchemaVer) LessThan(other SchemaVer) bool { if s.Model != other.Model { return s.Model < other.Model } if s.Revision != other.Revision { return s.Revision < other.Revision } return s.Addition < other.Addition } func (s SchemaVer) LessThanOrEqualTo(other SchemaVer) bool { return s.LessThan(other) || s.Equal(other) } func (s SchemaVer) Equal(other SchemaVer) bool { return s.Model == other.Model && s.Revision == other.Revision && s.Addition == other.Addition } func (s SchemaVer) GreaterThan(other SchemaVer) bool { if s.Model != other.Model { return s.Model > other.Model } if s.Revision != other.Revision { return s.Revision > other.Revision } return s.Addition > other.Addition } func (s SchemaVer) GreaterOrEqualTo(other SchemaVer) bool { return s.GreaterThan(other) || s.Equal(other) } ================================================ FILE: internal/schemaver/schema_ver_test.go ================================================ package schemaver import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSchemaVer_LessThan(t *testing.T) { tests := []struct { name string v1 SchemaVer v2 SchemaVer want bool }{ { name: "equal versions", v1: New(1, 0, 0), v2: New(1, 0, 0), want: false, }, { name: "different model versions", v1: New(1, 0, 0), v2: New(2, 0, 0), want: true, }, { name: "different revision versions", v1: New(1, 1, 0), v2: New(1, 2, 0), want: true, }, { name: "different addition versions", v1: New(1, 0, 1), v2: New(1, 0, 2), want: true, }, { name: "inverted addition versions", v1: New(1, 0, 2), v2: New(1, 0, 1), want: false, }, { name: "greater model overrides lower revision", v1: New(2, 0, 0), v2: New(1, 9, 9), want: false, }, { name: "greater revision overrides lower addition", v1: New(1, 2, 0), v2: New(1, 1, 9), want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.v1.LessThan(tt.v2)) }) } } func TestSchemaVer_GreaterOrEqualTo(t *testing.T) { tests := []struct { name string v1 SchemaVer v2 SchemaVer want bool }{ { name: "equal versions", v1: New(1, 0, 0), v2: New(1, 0, 0), want: true, }, { name: "different model versions", v1: New(1, 0, 0), v2: New(2, 0, 0), want: false, }, { name: "different revision versions", v1: New(1, 1, 0), v2: New(1, 2, 0), want: false, }, { name: "different addition versions", v1: New(1, 0, 1), v2: New(1, 0, 2), want: false, }, { name: "inverted addition versions", v1: New(1, 0, 2), v2: New(1, 0, 1), want: true, }, { name: "greater model overrides lower revision", v1: New(2, 0, 0), v2: New(1, 9, 9), want: true, }, { name: "greater revision overrides lower addition", v1: New(1, 2, 0), v2: New(1, 1, 9), want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.v1.GreaterOrEqualTo(tt.v2)) }) } } func TestSchemaVer_LessThanOrEqualTo(t *testing.T) { tests := []struct { name string v1 SchemaVer v2 SchemaVer want bool }{ { name: "equal versions", v1: New(1, 2, 3), v2: New(1, 2, 3), want: true, }, { name: "less than version", v1: New(1, 2, 3), v2: New(1, 2, 4), want: true, }, { name: "greater than version", v1: New(1, 2, 4), v2: New(1, 2, 3), want: false, }, { name: "different model - less", v1: New(1, 9, 9), v2: New(2, 0, 0), want: true, }, { name: "different model - greater", v1: New(2, 0, 0), v2: New(1, 9, 9), want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.v1.LessThanOrEqualTo(tt.v2)) }) } } func TestSchemaVer_Equal(t *testing.T) { tests := []struct { name string v1 SchemaVer v2 SchemaVer want bool }{ { name: "equal versions", v1: New(1, 2, 3), v2: New(1, 2, 3), want: true, }, { name: "different addition", v1: New(1, 2, 3), v2: New(1, 2, 4), want: false, }, { name: "different revision", v1: New(1, 2, 3), v2: New(1, 3, 3), want: false, }, { name: "different model", v1: New(1, 2, 3), v2: New(2, 2, 3), want: false, }, { name: "zero values equal", v1: New(1, 0, 0), v2: New(1, 0, 0), want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.v1.Equal(tt.v2)) }) } } func TestSchemaVer_GreaterThan(t *testing.T) { tests := []struct { name string v1 SchemaVer v2 SchemaVer want bool }{ { name: "equal versions", v1: New(1, 2, 3), v2: New(1, 2, 3), want: false, }, { name: "greater addition", v1: New(1, 2, 4), v2: New(1, 2, 3), want: true, }, { name: "greater revision", v1: New(1, 3, 0), v2: New(1, 2, 9), want: true, }, { name: "greater model", v1: New(2, 0, 0), v2: New(1, 9, 9), want: true, }, { name: "less than", v1: New(1, 2, 3), v2: New(1, 2, 4), want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.v1.GreaterThan(tt.v2)) }) } } func TestParse(t *testing.T) { tests := []struct { name string input string want SchemaVer wantErr bool }{ { name: "valid version", input: "1.2.3", want: New(1, 2, 3), wantErr: false, }, { name: "valid version with v prefix", input: "v1.2.3", want: New(1, 2, 3), wantErr: false, }, { name: "valid version with v prefix and zeros", input: "v1.0.0", want: New(1, 0, 0), wantErr: false, }, { name: "valid large numbers", input: "999.888.777", want: New(999, 888, 777), wantErr: false, }, { name: "valid with whitespace", input: " 1.2.3 ", want: New(1, 2, 3), wantErr: false, }, { name: "invalid version with zeros", input: "0.0.0", want: New(0, 0, 0), wantErr: true, }, { name: "invalid version with v prefix and zero model", input: "v0.0.0", want: New(0, 0, 0), wantErr: true, }, { name: "invalid empty string", input: "", wantErr: true, }, { name: "invalid too few parts", input: "1.2", wantErr: true, }, { name: "invalid too many parts", input: "1.2.3.4", wantErr: true, }, { name: "invalid non-numeric model", input: "a.2.3", wantErr: true, }, { name: "invalid non-numeric revision", input: "1.b.3", wantErr: true, }, { name: "invalid non-numeric addition", input: "1.2.c", wantErr: true, }, { name: "invalid negative number", input: "-1.2.3", wantErr: true, }, { name: "invalid format with spaces", input: "1 . 2 . 3", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Parse(tt.input) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && (got.Model != tt.want.Model || got.Revision != tt.want.Revision || got.Addition != tt.want.Addition) { t.Errorf("Parse() = %v, want %v", got, tt.want) } }) } } func TestSchemaVer_Valid(t *testing.T) { tests := []struct { name string schema SchemaVer expected bool }{ { name: "valid schema version - all positive", schema: SchemaVer{ Model: 1, Revision: 1, Addition: 1, }, expected: true, }, { name: "valid schema version - zero revision and addition", schema: SchemaVer{ Model: 1, Revision: 0, Addition: 0, }, expected: true, }, { name: "invalid - zero model", schema: SchemaVer{ Model: 0, Revision: 1, Addition: 1, }, expected: false, }, { name: "invalid - negative model", schema: SchemaVer{ Model: -1, Revision: 1, Addition: 1, }, expected: false, }, { name: "invalid - negative revision", schema: SchemaVer{ Model: 1, Revision: -1, Addition: 1, }, expected: false, }, { name: "invalid - negative addition", schema: SchemaVer{ Model: 1, Revision: 1, Addition: -1, }, expected: false, }, { name: "invalid - all negative", schema: SchemaVer{ Model: -1, Revision: -1, Addition: -1, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, tt.schema.Valid()) }) } } func TestSchemaVer_String(t *testing.T) { tests := []struct { name string schema SchemaVer want string }{ { name: "basic version", schema: New(1, 2, 3), want: "v1.2.3", }, { name: "version with zeros", schema: New(1, 0, 0), want: "v1.0.0", }, { name: "large numbers", schema: New(999, 888, 777), want: "v999.888.777", }, { name: "single digits", schema: New(5, 4, 3), want: "v5.4.3", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, tt.schema.String()) }) } } func TestSchemaVer_MarshalJSON(t *testing.T) { tests := []struct { name string schema SchemaVer want string }{ { name: "basic version", schema: New(1, 2, 3), want: `"v1.2.3"`, }, { name: "version with zeros", schema: New(1, 0, 0), want: `"v1.0.0"`, }, { name: "large numbers", schema: New(999, 888, 777), want: `"v999.888.777"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.schema.MarshalJSON() require.NoError(t, err) assert.Equal(t, tt.want, string(got)) }) } } func TestSchemaVer_UnmarshalJSON(t *testing.T) { tests := []struct { name string input string want SchemaVer wantErr require.ErrorAssertionFunc }{ { name: "valid version", input: `"v1.2.3"`, want: New(1, 2, 3), wantErr: require.NoError, }, { name: "valid version without v prefix", input: `"1.2.3"`, want: New(1, 2, 3), wantErr: require.NoError, }, { name: "valid version with zeros", input: `"v1.0.0"`, want: New(1, 0, 0), wantErr: require.NoError, }, { name: "invalid JSON format", input: `{"version": "v1.2.3"}`, wantErr: require.Error, }, { name: "invalid version format", input: `"invalid"`, wantErr: require.Error, }, { name: "invalid zero model", input: `"v0.1.2"`, wantErr: require.Error, }, { name: "malformed JSON", input: `"v1.2.3`, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got SchemaVer err := json.Unmarshal([]byte(tt.input), &got) tt.wantErr(t, err) if err == nil { assert.Equal(t, tt.want, got) } }) } } func TestSchemaVer_JSONRoundTrip(t *testing.T) { tests := []struct { name string schema SchemaVer }{ { name: "basic version", schema: New(1, 2, 3), }, { name: "version with zeros", schema: New(1, 0, 0), }, { name: "large numbers", schema: New(999, 888, 777), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // marshal data, err := json.Marshal(tt.schema) require.NoError(t, err) // unmarshal var got SchemaVer err = json.Unmarshal(data, &got) require.NoError(t, err) // should be equal assert.Equal(t, tt.schema, got) }) } } ================================================ FILE: internal/stringutil/color.go ================================================ package stringutil import "fmt" const ( DefaultColor Color = iota + 30 Red Green Yellow Blue Magenta Cyan White ) type Color uint8 // TODO: not cross platform (windows...) func (c Color) Format(s string) string { return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) } ================================================ FILE: internal/stringutil/parse.go ================================================ package stringutil import "regexp" // MatchCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map. func MatchCaptureGroups(regEx *regexp.Regexp, str string) map[string]string { match := regEx.FindStringSubmatch(str) results := make(map[string]string) for i, name := range regEx.SubexpNames() { if i > 0 && i <= len(match) { results[name] = match[i] } } return results } ================================================ FILE: internal/stringutil/string_helpers.go ================================================ package stringutil import ( "sort" "strings" ) // HasAnyOfSuffixes returns an indication if the given string has any of the given suffixes. func HasAnyOfSuffixes(input string, suffixes ...string) bool { for _, suffix := range suffixes { if strings.HasSuffix(input, suffix) { return true } } return false } // HasAnyOfPrefixes returns an indication if the given string has any of the given prefixes. func HasAnyOfPrefixes(input string, prefixes ...string) bool { for _, prefix := range prefixes { if strings.HasPrefix(input, prefix) { return true } } return false } // SplitCommaSeparatedString returns a slice of strings separated from the input string by commas func SplitCommaSeparatedString(input string) []string { output := make([]string, 0) for _, inputItem := range strings.Split(input, ",") { if len(inputItem) > 0 { output = append(output, inputItem) } } return output } // SplitOnFirstString splits the input string on the first occurrence of any of the provided separators. func SplitOnFirstString(s string, separators ...string) (before, after string) { minIdx := len(s) foundSep := "" for _, sep := range separators { if idx := strings.Index(s, sep); idx != -1 && idx < minIdx { minIdx = idx foundSep = sep } } if foundSep == "" { return s, "" } return s[:minIdx], s[minIdx+len(foundSep):] } func SplitOnAny(s string, separators ...string) []string { if s == "" { return nil } parts := []string{s} // sort separators by length in descending order to ensure longer separators are processed first. // This isn't foolproof, but it helps with common cases where longer separators should take precedence. separators = append([]string{}, separators...) sort.Slice(separators, func(i, j int) bool { return len(separators[i]) > len(separators[j]) }) for _, sep := range separators { var newParts []string for _, part := range parts { newParts = append(newParts, strings.Split(part, sep)...) } parts = newParts } return parts } ================================================ FILE: internal/stringutil/string_helpers_test.go ================================================ package stringutil import ( "testing" "github.com/stretchr/testify/assert" ) func TestHasAnyOfSuffixes(t *testing.T) { tests := []struct { name string input string suffixes []string expected bool }{ { name: "go case", input: "this has something", suffixes: []string{ "has something", "has NOT something", }, expected: true, }, { name: "no match", input: "this has something", suffixes: []string{ "has NOT something", }, expected: false, }, { name: "empty", input: "this has something", suffixes: []string{}, expected: false, }, { name: "positive match last", input: "this has something", suffixes: []string{ "that does not have", "something", }, expected: true, }, { name: "empty input", input: "", suffixes: []string{ "that does not have", "this has", }, expected: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert.Equal(t, test.expected, HasAnyOfSuffixes(test.input, test.suffixes...)) }) } } func TestHasAnyOfPrefixes(t *testing.T) { tests := []struct { name string input string prefixes []string expected bool }{ { name: "go case", input: "this has something", prefixes: []string{ "this has", "that does not have", }, expected: true, }, { name: "no match", input: "this has something", prefixes: []string{ "this DOES NOT has", "that does not have", }, expected: false, }, { name: "empty", input: "this has something", prefixes: []string{}, expected: false, }, { name: "positive match last", input: "this has something", prefixes: []string{ "that does not have", "this has", }, expected: true, }, { name: "empty input", input: "", prefixes: []string{ "that does not have", "this has", }, expected: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { assert.Equal(t, test.expected, HasAnyOfPrefixes(test.input, test.prefixes...)) }) } } func TestSplitCommaSeparatedString(t *testing.T) { tests := []struct { input string expected []string }{ { input: "testing", expected: []string{"testing"}, }, { input: "", expected: []string{}, }, { input: "testing1,testing2", expected: []string{"testing1", "testing2"}, }, { input: "testing1,,testing2,testing3", expected: []string{"testing1", "testing2", "testing3"}, }, { input: "testing1,testing2,,", expected: []string{"testing1", "testing2"}, }, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { assert.Equal(t, test.expected, SplitCommaSeparatedString(test.input)) }) } } func TestSplitOnFirstString(t *testing.T) { tests := []struct { name string input string separators []string wantBefore string wantAfter string }{ // go cases { name: "single separator found", input: "key=value", separators: []string{"="}, wantBefore: "key", wantAfter: "value", }, { name: "multiple separators, first one wins", input: "protocol://host:port", separators: []string{"://", ":"}, wantBefore: "protocol", wantAfter: "host:port", }, { name: "multiple separators, earlier position wins", input: "name:value=data", separators: []string{"=", ":"}, wantBefore: "name", wantAfter: "value=data", }, // edge cases { name: "no separator found", input: "noseparator", separators: []string{"=", ":"}, wantBefore: "noseparator", wantAfter: "", }, { name: "empty input", input: "", separators: []string{"="}, wantBefore: "", wantAfter: "", }, { name: "separator at beginning", input: "=value", separators: []string{"="}, wantBefore: "", wantAfter: "value", }, { name: "separator at end", input: "key=", separators: []string{"="}, wantBefore: "key", wantAfter: "", }, { name: "only separator", input: "=", separators: []string{"="}, wantBefore: "", wantAfter: "", }, // multiple occurrences { name: "multiple occurrences of same separator", input: "a=b=c=d", separators: []string{"="}, wantBefore: "a", wantAfter: "b=c=d", }, { name: "multiple different separators, choose earliest", input: "a:b=c:d", separators: []string{"=", ":"}, wantBefore: "a", wantAfter: "b=c:d", }, // multi-character separators { name: "multi-character separator", input: "before::after", separators: []string{"::"}, wantBefore: "before", wantAfter: "after", }, { name: "overlapping separators", input: "test:::data", separators: []string{"::", ":::"}, wantBefore: "test", wantAfter: ":data", }, { name: "longer separator wins when at same position", input: "test:::data", separators: []string{":::", "::"}, wantBefore: "test", wantAfter: "data", }, // more realistic cases { name: "URL parsing", input: "https://user:pass@host:8080/path?query=value", separators: []string{"://", "@", ":", "/", "?", "="}, wantBefore: "https", wantAfter: "user:pass@host:8080/path?query=value", }, { name: "environment variable", input: "PATH=/usr/bin:/bin", separators: []string{"=", ":"}, wantBefore: "PATH", wantAfter: "/usr/bin:/bin", }, { name: "docker image tag", input: "registry.example.com/namespace/image:v1.0", separators: []string{":", "/"}, wantBefore: "registry.example.com", wantAfter: "namespace/image:v1.0", }, // special characters { name: "unicode separators", input: "hello→world", separators: []string{"→"}, wantBefore: "hello", wantAfter: "world", }, { name: "whitespace separators", input: "word1 word2\tword3", separators: []string{" ", "\t"}, wantBefore: "word1", wantAfter: "word2\tword3", }, // co separators provided { name: "no separators provided", input: "test=data", separators: []string{}, wantBefore: "test=data", wantAfter: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotBefore, gotAfter := SplitOnFirstString(tt.input, tt.separators...) if gotBefore != tt.wantBefore { t.Errorf("SplitOnFirstString() gotBefore = %q, want %q", gotBefore, tt.wantBefore) } if gotAfter != tt.wantAfter { t.Errorf("SplitOnFirstString() gotAfter = %q, want %q", gotAfter, tt.wantAfter) } }) } } func TestSplitOnAny(t *testing.T) { tests := []struct { name string input string separators []string expected []string }{ { name: "empty string", input: "", separators: []string{","}, expected: nil, }, { name: "single separator", input: "a,b,c", separators: []string{","}, expected: []string{"a", "b", "c"}, }, { name: "multiple separators", input: "a,b;c:d", separators: []string{",", ";", ":"}, expected: []string{"a", "b", "c", "d"}, }, { name: "no separators found", input: "hello", separators: []string{",", ";"}, expected: []string{"hello"}, }, { name: "consecutive separators", input: "a,,b", separators: []string{","}, expected: []string{"a", "", "b"}, }, { name: "separator at beginning", input: ",a,b", separators: []string{","}, expected: []string{"", "a", "b"}, }, { name: "separator at end", input: "a,b,", separators: []string{","}, expected: []string{"a", "b", ""}, }, { name: "only separators", input: ",,", separators: []string{","}, expected: []string{"", "", ""}, }, { name: "overlapping separators", input: "a,b;c,d", separators: []string{",", ";"}, expected: []string{"a", "b", "c", "d"}, }, { name: "separator is substring of another", input: "a::b:c", separators: []string{"::", ":"}, expected: []string{"a", "b", "c"}, }, { name: "order does not matter for overlapping", input: "a::b:c", separators: []string{":", "::"}, expected: []string{"a", "b", "c"}, }, { name: "no separators provided", input: "hello", separators: []string{}, expected: []string{"hello"}, }, { name: "multi-character separator", input: "a<->b<->c", separators: []string{"<->"}, expected: []string{"a", "b", "c"}, }, { name: "mixed single and multi-character separators", input: "a,b<->c;d", separators: []string{",", "<->", ";"}, expected: []string{"a", "b", "c", "d"}, }, { name: "space separator", input: "hello world test", separators: []string{" "}, expected: []string{"hello", "world", "test"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := SplitOnAny(tt.input, tt.separators...) assert.Equal(t, tt.expected, result) }) } } ================================================ FILE: internal/stringutil/stringset.go ================================================ package stringutil type StringSet map[string]struct{} func NewStringSet() StringSet { return make(StringSet) } func NewStringSetFromSlice(start []string) StringSet { ret := make(StringSet) for _, s := range start { ret.Add(s) } return ret } func (s StringSet) Add(i string) { s[i] = struct{}{} } func (s StringSet) Remove(i string) { delete(s, i) } func (s StringSet) Contains(i string) bool { _, ok := s[i] return ok } func (s StringSet) ToSlice() []string { ret := make([]string, len(s)) idx := 0 for v := range s { ret[idx] = v idx++ } return ret } ================================================ FILE: internal/stringutil/tprint.go ================================================ package stringutil import ( "bytes" "text/template" ) // Tprintf renders a string from a given template string and field values func Tprintf(tmpl string, data map[string]interface{}) string { t := template.Must(template.New("").Parse(tmpl)) buf := &bytes.Buffer{} if err := t.Execute(buf, data); err != nil { return "" } return buf.String() } ================================================ FILE: internal/testutils/golden_file.go ================================================ package testutils import ( "io" "os" "path" "path/filepath" "strings" "testing" ) const ( TestDataDir = "testdata" GoldenFileDirName = "snapshot" GoldenFileExt = ".golden" GoldenFileDirPath = TestDataDir + string(filepath.Separator) + GoldenFileDirName ) // dangerText wraps text in ANSI escape codes for reverse red to make it highly visible. func dangerText(s string) string { return "\033[7;31m" + s + "\033[0m" } func GetGoldenFilePath(t *testing.T) string { t.Helper() // when using table-driven-tests, the `t.Name()` results in a string with slashes // which makes it impossible to reference in a filesystem, producing a "No such file or directory" filename := strings.ReplaceAll(t.Name(), "/", "_") return path.Join(GoldenFileDirPath, filename+GoldenFileExt) } func UpdateGoldenFileContents(t *testing.T, contents []byte) { t.Helper() goldenFilePath := GetGoldenFilePath(t) t.Log(dangerText("!!! UPDATING GOLDEN FILE !!!"), goldenFilePath) err := os.WriteFile(goldenFilePath, contents, 0600) if err != nil { t.Fatalf("could not update golden file (%s): %+v", goldenFilePath, err) } } func GetGoldenFileContents(t *testing.T) []byte { t.Helper() goldenPath := GetGoldenFilePath(t) if !fileOrDirExists(t, goldenPath) { t.Fatalf("golden file does not exist: %s", goldenPath) } f, err := os.Open(goldenPath) if err != nil { t.Fatalf("could not open file (%s): %+v", goldenPath, err) } defer f.Close() bytes, err := io.ReadAll(f) if err != nil { t.Fatalf("could not read file (%s): %+v", goldenPath, err) } return bytes } func fileOrDirExists(t *testing.T, filename string) bool { t.Helper() _, err := os.Stat(filename) return !os.IsNotExist(err) } ================================================ FILE: llms.txt ================================================ # Grype Grype is a vulnerability scanner for container images and filesystems developed by Anchore. It easily finds vulnerabilities for major operating system packages and language-specific packages. ## Key Features - Scans container images, filesystems, and SBOMs for known vulnerabilities - Supports major Linux distributions (Alpine, Ubuntu, Debian, RHEL, CentOS, etc.) - Language support for Java, JavaScript, Python, Go, Ruby, Rust, .NET, PHP, and more - Works with Docker, OCI, and Singularity image formats - Integrates with Syft for SBOM generation - Supports VEX (Vulnerability Exploitability Exchange) for filtering results - Risk scoring with EPSS (Exploit Prediction Scoring System) and CVSS metrics ## Architecture - Written in Go - Uses SQLite for vulnerability database storage - Modular matcher system for different package types and ecosystems - Automatic database updates from multiple vulnerability sources - CLI-first design with multiple output formats (table, JSON, SARIF, CycloneDX) ## Main Components - `cmd/grype/` - CLI application entry point - `grype/` - Core library with matchers, database, and scanning logic - `grype/matcher/` - Package-specific vulnerability matchers - `grype/db/` - Database management and vulnerability storage - `grype/pkg/` - Package identification and metadata - `grype/presenter/` - Output formatting (JSON, table, SARIF, etc.) ## Usage Basic vulnerability scan: ```bash grype ``` Scan with SBOM: ```bash grype sbom:./sbom.json ``` The tool automatically manages its vulnerability database and provides configurable output formats and filtering options. ================================================ FILE: schema/grype/db/README.md ================================================ # Grype v6 Database Schemas This directory contains the schemas for the Grype v6 vulnerability database. These schemas are automatically generated from the Go code definitions and are used to track schema evolution over time. ## What Are These Schemas? The Grype database has two types of schemas that are tracked: ### 1. SQL Schema (`sql/`) The SQLite table definitions (CREATE TABLE and CREATE INDEX statements) generated from GORM models in `grype/db/v6/models.go`. This captures: - Database tables and columns - Foreign key relationships - Indexes for query performance - All structural aspects of the database **Example tables:** `vulnerability_handles`, `packages`, `operating_systems`, `cpes`, `blobs`, etc. ### 2. Blob JSON Schema (`blob/json/`) A unified JSON schema for all blob types stored in the `blobs` table. The blobs are JSON data referenced by various handle tables and include: - `VulnerabilityBlob`: Core vulnerability advisory data - `PackageBlob`: Package version ranges and fix information - `KnownExploitedVulnerabilityBlob`: CISA KEV catalog data ## Schema Files Each schema type has two files per version: - **`schema-X.Y.Z.{sql|json}`**: The versioned schema file that should never be modified after creation - **`schema-latest.{sql|json}`**: A copy of the most recent version to show diffs in PR reviews The `-latest` files exist to make PR reviews easier. When you increment the schema version and regenerate, Git shows the new `schema-X.Y.Z` file as entirely new (just additions), which makes it hard to see what actually changed. The `-latest` file, however, is already tracked by Git, so it shows as a **diff** - making it easy to review exactly what changed in the schema. ## How to Regenerate Schemas When you make changes to: - GORM models in `grype/db/v6/models.go` - Blob types in `grype/db/v6/blobs.go` You need to regenerate the schemas: ```bash task generate-db-schema ``` This will: 1. Create an in-memory SQLite database with all GORM models 2. Extract the SQL schema from the database 3. Generate a unified JSON schema for all blob types 4. Write the schemas to versioned files ## What to Do When Schema Changes ### If You're Adding Compatible Changes (Addition) Examples: Adding a new optional field to a blob, adding a new index 1. Increment `Addition` in `grype/db/v6/db.go`: ```go Addition = 2 // was 1 ``` 2. Regenerate schemas: ```bash task generate-db-schema ``` 3. Commit the new schema files along with your code changes ### If You're Making Potentially Breaking Changes (Revision) Examples: Changing field types, removing optional fields, altering table structure 1. Increment `Revision` in `grype/db/v6/db.go` and reset `Addition`: ```go Revision = 2 // was 1 Addition = 0 // reset ``` 2. Regenerate and commit as above ### If You're Making Definitely Breaking Changes (Model) Please meet with the team about this - it requires careful planning and should be rare. ## Versioning Rules (SchemaVer) This project uses [SchemaVer](https://docs.snowplowanalytics.com/docs/pipeline-components-and-applications/iglu/common-architecture/schemaver/) for schema versioning: `MODEL.REVISION.ADDITION` - **MODEL**: Increment for breaking changes that prevent interaction with ALL historical data - **REVISION**: Increment for changes that may prevent interaction with SOME historical data - **ADDITION**: Increment for changes that are compatible with ALL historical data **Important:** Never delete or modify existing versioned schema files! Only add new versions. ## CI Drift Detection The static analysis CI check runs: ```bash task check-db-schema-drift ``` This: 1. Checks that working directory is clean 2. Runs `task generate-db-schema` 3. Checks if any schema files changed If schemas changed but weren't committed, the check fails. This ensures: - Schema changes are always tracked - Version numbers are incremented appropriately - Code changes and schema changes stay in sync This catches cases where you modified models or blob types but forgot to regenerate and commit the schemas. ## Common Errors ### "Cowardly refusing to overwrite existing schema" This means: - The schema has changed (code differs from committed schema) - But the version number hasn't been incremented **Solution:** Increment the appropriate version constant in `grype/db/v6/db.go` ### "Database blob schemas have uncommitted changes" This means: - You made schema changes - Regenerated the schemas - But haven't committed the new schema files **Solution:** Add and commit the schema files in `schema/grype/db/` ## More Information - **Generator Code**: `grype/db/v6/schema/main.go` - The Go program that generates these schemas - **Version Constants**: `grype/db/v6/db.go` - Where `ModelVersion`, `Revision`, and `Addition` are defined - **GORM Models**: `grype/db/v6/models.go` - The source for SQL schema generation - **Blob Types**: `grype/db/v6/blobs.go` - The source for blob JSON schema generation ================================================ FILE: schema/grype/db/blob/json/schema-6.1.1.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db/blob/json/6.1.1", "$defs": { "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploitedVulnerabilityBlob": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string", "format": "date-time" }, "required_action": { "type": "string" }, "due_date": { "type": "string", "format": "date-time" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityBlob": { "$defs": { "aliases": { "description": "is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases." }, "assigner": { "description": "is a list of names, email, or organizations who submitted the vulnerability" }, "description": { "description": "of the vulnerability as provided by the source" }, "id": { "description": "is the lowercase unique string identifier for the vulnerability relative to the provider" }, "refs": { "description": "are URLs to external resources that provide more information about the vulnerability" }, "severities": { "description": "is a list of severity indications (quantitative or qualitative) for the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" } }, "type": "object", "required": [ "id" ] } }, "oneOf": [ { "$ref": "#/$defs/VulnerabilityBlob" }, { "$ref": "#/$defs/PackageBlob" }, { "$ref": "#/$defs/KnownExploitedVulnerabilityBlob" } ], "description": "Unified schema for all blob types stored in the Grype v6 database" } ================================================ FILE: schema/grype/db/blob/json/schema-6.1.2.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db/blob/json/6.1.2", "$defs": { "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploitedVulnerabilityBlob": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string", "format": "date-time" }, "required_action": { "type": "string" }, "due_date": { "type": "string", "format": "date-time" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityBlob": { "$defs": { "aliases": { "description": "is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases." }, "assigner": { "description": "is a list of names, email, or organizations who submitted the vulnerability" }, "description": { "description": "of the vulnerability as provided by the source" }, "id": { "description": "is the lowercase unique string identifier for the vulnerability relative to the provider" }, "refs": { "description": "are URLs to external resources that provide more information about the vulnerability" }, "severities": { "description": "is a list of severity indications (quantitative or qualitative) for the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" } }, "type": "object", "required": [ "id" ] } }, "oneOf": [ { "$ref": "#/$defs/VulnerabilityBlob" }, { "$ref": "#/$defs/PackageBlob" }, { "$ref": "#/$defs/KnownExploitedVulnerabilityBlob" } ], "description": "Unified schema for all blob types stored in the Grype v6 database" } ================================================ FILE: schema/grype/db/blob/json/schema-6.1.3.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db/blob/json/6.1.3", "$defs": { "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploitedVulnerabilityBlob": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string", "format": "date-time" }, "required_action": { "type": "string" }, "due_date": { "type": "string", "format": "date-time" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityBlob": { "$defs": { "aliases": { "description": "is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases." }, "assigner": { "description": "is a list of names, email, or organizations who submitted the vulnerability" }, "description": { "description": "of the vulnerability as provided by the source" }, "id": { "description": "is the lowercase unique string identifier for the vulnerability relative to the provider" }, "refs": { "description": "are URLs to external resources that provide more information about the vulnerability" }, "severities": { "description": "is a list of severity indications (quantitative or qualitative) for the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" } }, "type": "object", "required": [ "id" ] } }, "oneOf": [ { "$ref": "#/$defs/VulnerabilityBlob" }, { "$ref": "#/$defs/PackageBlob" }, { "$ref": "#/$defs/KnownExploitedVulnerabilityBlob" } ], "description": "Unified schema for all blob types stored in the Grype v6 database" } ================================================ FILE: schema/grype/db/blob/json/schema-6.1.4.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db/blob/json/6.1.4", "$defs": { "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploitedVulnerabilityBlob": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string", "format": "date-time" }, "required_action": { "type": "string" }, "due_date": { "type": "string", "format": "date-time" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityBlob": { "$defs": { "aliases": { "description": "is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases." }, "assigner": { "description": "is a list of names, email, or organizations who submitted the vulnerability" }, "description": { "description": "of the vulnerability as provided by the source" }, "id": { "description": "is the lowercase unique string identifier for the vulnerability relative to the provider" }, "refs": { "description": "are URLs to external resources that provide more information about the vulnerability" }, "severities": { "description": "is a list of severity indications (quantitative or qualitative) for the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" } }, "type": "object", "required": [ "id" ] } }, "oneOf": [ { "$ref": "#/$defs/VulnerabilityBlob" }, { "$ref": "#/$defs/PackageBlob" }, { "$ref": "#/$defs/KnownExploitedVulnerabilityBlob" } ], "description": "Unified schema for all blob types stored in the Grype v6 database" } ================================================ FILE: schema/grype/db/blob/json/schema-latest.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db/blob/json/6.1.4", "$defs": { "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploitedVulnerabilityBlob": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string", "format": "date-time" }, "required_action": { "type": "string" }, "due_date": { "type": "string", "format": "date-time" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityBlob": { "$defs": { "aliases": { "description": "is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases." }, "assigner": { "description": "is a list of names, email, or organizations who submitted the vulnerability" }, "description": { "description": "of the vulnerability as provided by the source" }, "id": { "description": "is the lowercase unique string identifier for the vulnerability relative to the provider" }, "refs": { "description": "are URLs to external resources that provide more information about the vulnerability" }, "severities": { "description": "is a list of severity indications (quantitative or qualitative) for the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" } }, "type": "object", "required": [ "id" ] } }, "oneOf": [ { "$ref": "#/$defs/VulnerabilityBlob" }, { "$ref": "#/$defs/PackageBlob" }, { "$ref": "#/$defs/KnownExploitedVulnerabilityBlob" } ], "description": "Unified schema for all blob types stored in the Grype v6 database" } ================================================ FILE: schema/grype/db/sql/schema-6.1.1.sql ================================================ -- Generated by grype/db/v6/schema -- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update. -- Schema version: 6.1.1 CREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL); CREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text); CREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL); CREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL); CREATE TABLE `epss_metadata` (`date` datetime NOT NULL); CREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer); CREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`)); CREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text); CREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`); CREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`)); CREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text); CREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`)); CREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`)); CREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`); -- Indexes CREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE); CREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`); CREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`); CREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE); CREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE); CREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`); CREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`); CREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE); CREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`); CREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`); CREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE); CREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`); CREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`); CREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`); CREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`); CREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE); CREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE); CREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE); CREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`); ================================================ FILE: schema/grype/db/sql/schema-6.1.2.sql ================================================ -- Generated by grype/db/v6/schema -- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update. -- Schema version: 6.1.2 CREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL); CREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text); CREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text); CREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL); CREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL); CREATE TABLE `epss_metadata` (`date` datetime NOT NULL); CREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer); CREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`)); CREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text); CREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`); CREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`)); CREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text); CREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`)); CREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`)); CREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`); -- Indexes CREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE); CREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE); CREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`); CREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`); CREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE); CREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE); CREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`); CREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`); CREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE); CREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`); CREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`); CREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE); CREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`); CREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`); CREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`); CREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`); CREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE); CREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE); CREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE); CREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`); ================================================ FILE: schema/grype/db/sql/schema-6.1.3.sql ================================================ -- Generated by grype/db/v6/schema -- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update. -- Schema version: 6.1.3 CREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL); CREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text); CREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text); CREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL); CREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL); CREATE TABLE `epss_metadata` (`date` datetime NOT NULL); CREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer); CREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`)); CREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text); CREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`); CREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`)); CREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text); CREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`)); CREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`)); CREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`); -- Indexes CREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE); CREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE); CREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`); CREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`); CREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE); CREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE); CREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`); CREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`); CREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE); CREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`); CREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`); CREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE); CREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`); CREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`); CREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`); CREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`); CREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE); CREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE); CREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE); CREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`); ================================================ FILE: schema/grype/db/sql/schema-6.1.4.sql ================================================ -- Generated by grype/db/v6/schema -- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update. -- Schema version: 6.1.4 CREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL); CREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text); CREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text); CREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL); CREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL); CREATE TABLE `epss_metadata` (`date` datetime NOT NULL); CREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer); CREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`)); CREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text,`eol_date` datetime,`eoas_date` datetime); CREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`); CREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`)); CREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text); CREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`)); CREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`)); CREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`); -- Indexes CREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE); CREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE); CREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`); CREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`); CREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE); CREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE); CREATE INDEX `idx_operating_systems_eol_date` ON `operating_systems`(`eol_date`); CREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`); CREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`); CREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE); CREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`); CREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`); CREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE); CREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`); CREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`); CREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`); CREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`); CREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE); CREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE); CREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE); CREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`); ================================================ FILE: schema/grype/db/sql/schema-latest.sql ================================================ -- Generated by grype/db/v6/schema -- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update. -- Schema version: 6.1.4 CREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL); CREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text); CREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text); CREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL); CREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL); CREATE TABLE `epss_metadata` (`date` datetime NOT NULL); CREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer); CREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`)); CREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text,`eol_date` datetime,`eoas_date` datetime); CREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`); CREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`)); CREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text); CREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`)); CREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`); CREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`)); CREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`); -- Indexes CREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE); CREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE); CREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`); CREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`); CREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE); CREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE); CREATE INDEX `idx_operating_systems_eol_date` ON `operating_systems`(`eol_date`); CREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`); CREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`); CREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE); CREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`); CREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`); CREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`); CREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`); CREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE); CREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`); CREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`); CREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`); CREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`); CREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE); CREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE); CREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE); CREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE); CREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`); ================================================ FILE: schema/grype/db-search/json/README.md ================================================ # `db-search` JSON Schema This is the JSON schema for output from the `grype db search` command. The required inputs for defining the JSON schema are as follows: - the value of `cmd/grype/cli/commands/internal/dbsearch.MatchesSchemaVersion` that governs the schema version - the `Matches` type definition within `github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch/matches.go` that governs the overall document shape ## Versioning Versioning the JSON schema must be done manually by changing the `MatchesSchemaVersion` constant within `cmd/grype/cli/commands/internal/dbsearch/versions.go`. This schema is being versioned based off of the "SchemaVer" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models. Given a version number format `MODEL.REVISION.ADDITION`: - `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data - `REVISION`: increment when you make a schema change which may prevent interaction with some historical data - `ADDITION`: increment when you make a schema change that is compatible with all historical data ## Generating a New Schema Create the new schema by running `make generate-json-schema` from the root of the repo: - If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/grype/db-search/json/schema-$VERSION.json` - If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken - If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section) ***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. ================================================ FILE: schema/grype/db-search/json/schema-1.0.0.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.0.0/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/AffectedPackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/AffectedRange" }, "type": "array" } }, "type": "object" }, "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "detail": { "$ref": "#/$defs/AffectedPackageBlob" } }, "type": "object", "required": [ "detail" ] }, "AffectedPackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "AffectedRange": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/AffectedVersion" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "AffectedVersion": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixDetail": { "$defs": { "git_commit": { "description": "is the identifier for the Git commit associated with the fix." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." }, "timestamp": { "description": "is the date and time when the fix was committed." } }, "properties": { "git_commit": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "VulnerabilityInfo": { "$defs": { "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.0.1.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.0.1/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/AffectedPackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/AffectedRange" }, "type": "array" } }, "type": "object" }, "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "detail": { "$ref": "#/$defs/AffectedPackageBlob" } }, "type": "object", "required": [ "detail" ] }, "AffectedPackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "AffectedRange": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/AffectedVersion" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "AffectedVersion": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixDetail": { "$defs": { "git_commit": { "description": "is the identifier for the Git commit associated with the fix." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." }, "timestamp": { "description": "is the date and time when the fix was committed." } }, "properties": { "git_commit": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "VulnerabilityInfo": { "$defs": { "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.0.2.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.0.2/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/AffectedPackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/AffectedRange" }, "type": "array" } }, "type": "object" }, "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/AffectedPackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "AffectedPackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "AffectedRange": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/AffectedVersion" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "AffectedVersion": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixDetail": { "$defs": { "git_commit": { "description": "is the identifier for the Git commit associated with the fix." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." }, "timestamp": { "description": "is the date and time when the fix was committed." } }, "properties": { "git_commit": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "VulnerabilityInfo": { "$defs": { "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.0.3.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.0.3/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/AffectedPackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/AffectedRange" }, "type": "array" } }, "type": "object" }, "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/AffectedPackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "AffectedPackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "AffectedRange": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/AffectedVersion" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "AffectedVersion": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixDetail": { "$defs": { "git_commit": { "description": "is the identifier for the Git commit associated with the fix." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." }, "timestamp": { "description": "is the date and time when the fix was committed." } }, "properties": { "git_commit": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "VulnerabilityInfo": { "$defs": { "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "severity": { "description": "is the single string representation of the vulnerability's severity based on the set of available severity values" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.1.0.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.1.0/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/AffectedPackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/AffectedRange" }, "type": "array" } }, "type": "object" }, "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/AffectedPackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "AffectedPackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "AffectedRange": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/AffectedVersion" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "AffectedVersion": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "VulnerabilityInfo": { "$defs": { "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "severity": { "description": "is the single string representation of the vulnerability's severity based on the set of available severity values" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.1.1.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.1.1/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/PackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityInfo": { "$defs": { "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "severity": { "description": "is the single string representation of the vulnerability's severity based on the set of available severity values" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.1.2.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.1.2/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/PackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "CWE": { "properties": { "cve": { "type": "string" }, "cwe": { "type": "string" }, "source": { "type": "string" }, "type": { "type": "string" } }, "type": "object", "required": [ "cve", "cwe", "source", "type" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityInfo": { "$defs": { "cwes": { "description": "is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability" }, "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "severity": { "description": "is the single string representation of the vulnerability's severity based on the set of available severity values" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "cwes": { "items": { "$ref": "#/$defs/CWE" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-1.1.3.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.1.3/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\n\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/PackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "CWE": { "properties": { "cve": { "type": "string" }, "cwe": { "type": "string" }, "source": { "type": "string" }, "type": { "type": "string" } }, "type": "object", "required": [ "cve", "cwe", "source", "type" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityInfo": { "$defs": { "cwes": { "description": "is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability" }, "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "severity": { "description": "is the single string representation of the vulnerability's severity based on the set of available severity values" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "cwes": { "items": { "$ref": "#/$defs/CWE" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search/json/schema-latest.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search/json/1.1.3/matches", "$ref": "#/$defs/Matches", "$defs": { "AffectedPackageInfo": { "$defs": { "cpe": { "description": "is a Common Platform Enumeration that is affected by the vulnerability" }, "detail": { "description": "is the detailed information about the affected package" }, "namespace": { "description": "is a holdover value from the v5 DB schema that combines provider and search methods into a single value\n\nDeprecated: this field will be removed in a later version of the search schema" }, "os": { "description": "identifies the operating system release that the affected package is released for" }, "package": { "description": "identifies the name of the package in a specific ecosystem affected by the vulnerability" } }, "properties": { "os": { "$ref": "#/$defs/OperatingSystem" }, "package": { "$ref": "#/$defs/Package" }, "cpe": { "$ref": "#/$defs/CPE" }, "namespace": { "type": "string" }, "detail": { "$ref": "#/$defs/PackageBlob" } }, "type": "object", "required": [ "namespace", "detail" ] }, "CPE": { "properties": { "ID": { "type": "integer" }, "Part": { "type": "string" }, "Vendor": { "type": "string" }, "Product": { "type": "string" }, "Edition": { "type": "string" }, "Language": { "type": "string" }, "SoftwareEdition": { "type": "string" }, "TargetHardware": { "type": "string" }, "TargetSoftware": { "type": "string" }, "Other": { "type": "string" }, "Packages": { "items": { "$ref": "#/$defs/Package" }, "type": "array" } }, "type": "object", "required": [ "ID", "Part", "Vendor", "Product", "Edition", "Language", "SoftwareEdition", "TargetHardware", "TargetSoftware", "Other", "Packages" ] }, "CWE": { "properties": { "cve": { "type": "string" }, "cwe": { "type": "string" }, "source": { "type": "string" }, "type": { "type": "string" } }, "type": "object", "required": [ "cve", "cwe", "source", "type" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "Fix": { "$defs": { "detail": { "description": "provides additional fix information, such as commit details." }, "state": { "description": "represents the status of the fix (e.g., 'fixed', 'unaffected')." }, "version": { "description": "is the version number of the fix." } }, "properties": { "version": { "type": "string" }, "state": { "type": "string" }, "detail": { "$ref": "#/$defs/FixDetail" } }, "type": "object" }, "FixAvailability": { "$defs": { "date": { "description": "is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged." }, "kind": { "description": "describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)" } }, "properties": { "date": { "type": "string", "format": "date-time" }, "kind": { "type": "string" } }, "type": "object" }, "FixDetail": { "$defs": { "available": { "description": "indicates when the fix information became available and how it was obtained." }, "references": { "description": "contains URLs or identifiers for additional resources on the fix." } }, "properties": { "available": { "$ref": "#/$defs/FixAvailability" }, "references": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" } }, "type": "object" }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "Match": { "$defs": { "packages": { "description": "is the list of packages affected by the vulnerability." }, "vulnerability": { "description": "is the core advisory record for a single known vulnerability from a specific provider." } }, "properties": { "vulnerability": { "$ref": "#/$defs/VulnerabilityInfo" }, "packages": { "items": { "$ref": "#/$defs/AffectedPackageInfo" }, "type": "array" } }, "type": "object", "required": [ "vulnerability", "packages" ] }, "Matches": { "items": { "$ref": "#/$defs/Match" }, "type": "array" }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Package": { "properties": { "name": { "type": "string" }, "ecosystem": { "type": "string" } }, "type": "object", "required": [ "name", "ecosystem" ] }, "PackageBlob": { "$defs": { "cves": { "description": "is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability." }, "qualifiers": { "description": "are package attributes that confirm the package is affected by the vulnerability." }, "ranges": { "description": "specifies the affected version ranges and fixes if available." } }, "properties": { "cves": { "items": { "type": "string" }, "type": "array" }, "qualifiers": { "$ref": "#/$defs/PackageQualifiers" }, "ranges": { "items": { "$ref": "#/$defs/Range" }, "type": "array" } }, "type": "object" }, "PackageQualifiers": { "$defs": { "platform_cpes": { "description": "lists Common Platform Enumeration (CPE) identifiers for affected platforms." }, "rpm_modularity": { "description": "indicates if the package follows RPM modularity for versioning." } }, "properties": { "rpm_modularity": { "type": "string" }, "platform_cpes": { "items": { "type": "string" }, "type": "array" } }, "type": "object" }, "Range": { "$defs": { "fix": { "description": "provides details on the fix version and its state if available." }, "version": { "description": "defines the version constraints for affected software." } }, "properties": { "version": { "$ref": "#/$defs/Version" }, "fix": { "$ref": "#/$defs/Fix" } }, "type": "object" }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Version": { "$defs": { "constraint": { "description": "defines the version range constraint for affected versions." }, "type": { "description": "specifies the versioning system used (e.g., 'semver', 'rpm')." } }, "properties": { "type": { "type": "string" }, "constraint": { "type": "string" } }, "type": "object" }, "VulnerabilityInfo": { "$defs": { "cwes": { "description": "is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability" }, "epss": { "description": "is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability" }, "known_exploited": { "description": "is a list of known exploited vulnerabilities from the CISA KEV dataset" }, "modified_date": { "description": "is the date the vulnerability record was last modified" }, "provider": { "description": "is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions)." }, "published_date": { "description": "is the date the vulnerability record was first published" }, "severity": { "description": "is the single string representation of the vulnerability's severity based on the set of available severity values" }, "status": { "description": "conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')" }, "withdrawn_date": { "description": "is the date the vulnerability record was withdrawn" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "cwes": { "items": { "$ref": "#/$defs/CWE" }, "type": "array" } }, "type": "object", "required": [ "id", "provider", "status" ] } } } ================================================ FILE: schema/grype/db-search-vuln/json/README.md ================================================ # `db-search vuln` JSON Schema This is the JSON schema for output from the `grype db search vuln` command. The required inputs for defining the JSON schema are as follows: - the value of `cmd/grype/cli/commands/internal/dbsearch.VulnerabilitiesSchemaVersion` that governs the schema version - the `Vulnerabilities` type definition within `github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go` that governs the overall document shape ## Versioning Versioning the JSON schema must be done manually by changing the `VulnerabilitiesSchemaVersion` constant within `cmd/grype/cli/commands/internal/dbsearch/versions.go`. This schema is being versioned based off of the "SchemaVer" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models. Given a version number format `MODEL.REVISION.ADDITION`: - `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data - `REVISION`: increment when you make a schema change which may prevent interaction with some historical data - `ADDITION`: increment when you make a schema change that is compatible with all historical data ## Generating a New Schema Create the new schema by running `make generate-json-schema` from the root of the repo: - If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/grype/db-search-vuln/json/schema-$VERSION.json` - If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken - If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section) ***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. ================================================ FILE: schema/grype/db-search-vuln/json/schema-1.0.0.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.0/vulnerabilities", "$ref": "#/$defs/Vulnerabilities", "$defs": { "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "type": "array" }, "Vulnerability": { "$defs": { "affected_packages": { "description": "is the number of packages affected by the vulnerability" }, "operating_systems": { "description": "is a list of operating systems affected by the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "operating_systems": { "items": { "$ref": "#/$defs/OperatingSystem" }, "type": "array" }, "affected_packages": { "type": "integer" } }, "type": "object", "required": [ "id", "provider", "status", "operating_systems", "affected_packages" ] } } } ================================================ FILE: schema/grype/db-search-vuln/json/schema-1.0.1.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.1/vulnerabilities", "$ref": "#/$defs/Vulnerabilities", "$defs": { "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "type": "array" }, "Vulnerability": { "$defs": { "affected_packages": { "description": "is the number of packages affected by the vulnerability" }, "operating_systems": { "description": "is a list of operating systems affected by the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "operating_systems": { "items": { "$ref": "#/$defs/OperatingSystem" }, "type": "array" }, "affected_packages": { "type": "integer" } }, "type": "object", "required": [ "id", "provider", "status", "operating_systems", "affected_packages" ] } } } ================================================ FILE: schema/grype/db-search-vuln/json/schema-1.0.3.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.3/vulnerabilities", "$ref": "#/$defs/Vulnerabilities", "$defs": { "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "type": "array" }, "Vulnerability": { "$defs": { "affected_packages": { "description": "is the number of packages affected by the vulnerability" }, "operating_systems": { "description": "is a list of operating systems affected by the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "operating_systems": { "items": { "$ref": "#/$defs/OperatingSystem" }, "type": "array" }, "affected_packages": { "type": "integer" } }, "type": "object", "required": [ "id", "provider", "status", "operating_systems", "affected_packages" ] } } } ================================================ FILE: schema/grype/db-search-vuln/json/schema-1.0.4.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.4/vulnerabilities", "$ref": "#/$defs/Vulnerabilities", "$defs": { "CWE": { "properties": { "cve": { "type": "string" }, "cwe": { "type": "string" }, "source": { "type": "string" }, "type": { "type": "string" } }, "type": "object", "required": [ "cve", "cwe", "source", "type" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Reference": { "$defs": { "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "type": "array" }, "Vulnerability": { "$defs": { "affected_packages": { "description": "is the number of packages affected by the vulnerability" }, "operating_systems": { "description": "is a list of operating systems affected by the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "cwes": { "items": { "$ref": "#/$defs/CWE" }, "type": "array" }, "operating_systems": { "items": { "$ref": "#/$defs/OperatingSystem" }, "type": "array" }, "affected_packages": { "type": "integer" } }, "type": "object", "required": [ "id", "provider", "status", "operating_systems", "affected_packages" ] } } } ================================================ FILE: schema/grype/db-search-vuln/json/schema-1.0.5.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.5/vulnerabilities", "$ref": "#/$defs/Vulnerabilities", "$defs": { "CWE": { "properties": { "cve": { "type": "string" }, "cwe": { "type": "string" }, "source": { "type": "string" }, "type": { "type": "string" } }, "type": "object", "required": [ "cve", "cwe", "source", "type" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "type": "array" }, "Vulnerability": { "$defs": { "affected_packages": { "description": "is the number of packages affected by the vulnerability" }, "operating_systems": { "description": "is a list of operating systems affected by the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "cwes": { "items": { "$ref": "#/$defs/CWE" }, "type": "array" }, "operating_systems": { "items": { "$ref": "#/$defs/OperatingSystem" }, "type": "array" }, "affected_packages": { "type": "integer" } }, "type": "object", "required": [ "id", "provider", "status", "operating_systems", "affected_packages" ] } } } ================================================ FILE: schema/grype/db-search-vuln/json/schema-latest.json ================================================ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "anchore.io/schema/grype/db-search-vuln/json/1.0.5/vulnerabilities", "$ref": "#/$defs/Vulnerabilities", "$defs": { "CWE": { "properties": { "cve": { "type": "string" }, "cwe": { "type": "string" }, "source": { "type": "string" }, "type": { "type": "string" } }, "type": "object", "required": [ "cve", "cwe", "source", "type" ] }, "EPSS": { "properties": { "cve": { "type": "string" }, "epss": { "type": "number" }, "percentile": { "type": "number" }, "date": { "type": "string" } }, "type": "object", "required": [ "cve", "epss", "percentile", "date" ] }, "KnownExploited": { "properties": { "cve": { "type": "string" }, "vendor_project": { "type": "string" }, "product": { "type": "string" }, "date_added": { "type": "string" }, "required_action": { "type": "string" }, "due_date": { "type": "string" }, "known_ransomware_campaign_use": { "type": "string" }, "notes": { "type": "string" }, "urls": { "items": { "type": "string" }, "type": "array" }, "cwes": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "cve", "known_ransomware_campaign_use" ] }, "OperatingSystem": { "properties": { "name": { "type": "string" }, "version": { "type": "string" } }, "type": "object", "required": [ "name", "version" ] }, "Reference": { "$defs": { "id": { "description": "is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')" }, "tags": { "description": "is a free-form organizational field to convey additional information about the reference" }, "url": { "description": "is the external resource" } }, "properties": { "url": { "type": "string" }, "id": { "type": "string" }, "tags": { "items": { "type": "string" }, "type": "array" } }, "type": "object", "required": [ "url" ] }, "Severity": { "$defs": { "rank": { "description": "is a free-form organizational field to convey priority over other severities" }, "scheme": { "description": "describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)" }, "source": { "description": "is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')" }, "value": { "description": "is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N', or 'high' )" } }, "properties": { "scheme": { "type": "string" }, "value": true, "source": { "type": "string" }, "rank": { "type": "integer" } }, "type": "object", "required": [ "scheme", "value", "rank" ] }, "Vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "type": "array" }, "Vulnerability": { "$defs": { "affected_packages": { "description": "is the number of packages affected by the vulnerability" }, "operating_systems": { "description": "is a list of operating systems affected by the vulnerability" } }, "properties": { "id": { "type": "string" }, "assigner": { "items": { "type": "string" }, "type": "array" }, "description": { "type": "string" }, "refs": { "items": { "$ref": "#/$defs/Reference" }, "type": "array" }, "aliases": { "items": { "type": "string" }, "type": "array" }, "severities": { "items": { "$ref": "#/$defs/Severity" }, "type": "array" }, "severity": { "type": "string" }, "provider": { "type": "string" }, "status": { "type": "string" }, "published_date": { "type": "string", "format": "date-time" }, "modified_date": { "type": "string", "format": "date-time" }, "withdrawn_date": { "type": "string", "format": "date-time" }, "known_exploited": { "items": { "$ref": "#/$defs/KnownExploited" }, "type": "array" }, "epss": { "items": { "$ref": "#/$defs/EPSS" }, "type": "array" }, "cwes": { "items": { "$ref": "#/$defs/CWE" }, "type": "array" }, "operating_systems": { "items": { "$ref": "#/$defs/OperatingSystem" }, "type": "array" }, "affected_packages": { "type": "integer" } }, "type": "object", "required": [ "id", "provider", "status", "operating_systems", "affected_packages" ] } } } ================================================ FILE: templates/README.md ================================================ # Grype Templates This folder contains a set of "helper" go templates you can use for your own reports. Please feel free to extend and/or update the templates for your needs, be sure to contribute back into this folder any new templates! Current templates:
.
├── README.md
├── html.tmpl
├── junit.tmpl
├── csv.tmpl
└── table.tmpl
## Table This template mimics the "default" table output of Grype, there are some drawbacks to using the template vs the native output such as: - unsorted - duplicate rows - no (wont-fix) logic As you can see from the above list, it's not perfect but it's a start. ## HTML Produces a nice html template with a dynamic table using datatables.js. You can also modify the templating filter to limit the output to a subset. Default includes all ``` {{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") (eq $vuln.Vulnerability.Severity "Low") (eq $vuln.Vulnerability.Severity "Unknown") }} ``` We can limit it to only Critical, High, and Medium by editing the filter as follows ``` {{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") }} ``` ================================================ FILE: templates/csv.tmpl ================================================ "Package","Version Installed","Vulnerability ID","Severity" {{- range .Matches}} "{{.Artifact.Name}}","{{.Artifact.Version}}","{{.Vulnerability.ID}}","{{.Vulnerability.Severity}}" {{- end}} ================================================ FILE: templates/html.tmpl ================================================ Vulnerability Report {{/* Initialize counters */}} {{- $CountCritical := 0 }} {{- $CountHigh := 0 }} {{- $CountMedium := 0 }} {{- $CountLow := 0}} {{- $CountUnknown := 0 }} {{/* Create a list */}} {{- $FilteredMatches := list }} {{/* Loop through all vulns limit output and set count*/}} {{- range $vuln := .Matches }} {{/* Use this filter to exclude severity if needed */}} {{- if or (eq $vuln.Vulnerability.Severity "Critical") (eq $vuln.Vulnerability.Severity "High") (eq $vuln.Vulnerability.Severity "Medium") (eq $vuln.Vulnerability.Severity "Low") (eq $vuln.Vulnerability.Severity "Unknown") }} {{- $FilteredMatches = append $FilteredMatches $vuln }} {{- if eq $vuln.Vulnerability.Severity "Critical" }} {{- $CountCritical = add $CountCritical 1 }} {{- else if eq $vuln.Vulnerability.Severity "High" }} {{- $CountHigh = add $CountHigh 1 }} {{- else if eq $vuln.Vulnerability.Severity "Medium" }} {{- $CountMedium = add $CountMedium 1 }} {{- else if eq $vuln.Vulnerability.Severity "Low" }} {{- $CountLow = add $CountLow 1 }} {{- else }} {{- $CountUnknown = add $CountUnknown 1 }} {{- end }} {{- end }} {{- end }}

Vulnerability Report

Name:
{{- if eq (.Source.Type) "image" -}} {{.Source.Target.UserInput}} {{- else if eq (.Source.Type) "directory" -}} {{.Source.Target}} {{- else if eq (.Source.Type) "file" -}} {{.Source.Target}} {{- else -}} unknown {{- end -}}
Type:
{{ .Source.Type }}
{{- /* Conditionally add ImageID (Checksum) for images */ -}} {{- if eq .Source.Type "image" -}} {{- with .Source.Target.ID -}}
Checksum:
{{ . }}
{{- end -}} {{- end -}}
Date:
{{.Descriptor.Timestamp}}
Grype Logo
Critical
{{ $CountCritical }}
High
{{ $CountHigh }}
Medium
{{ $CountMedium }}
Low
{{ $CountLow }}
Unknown
{{ $CountUnknown }}
{{- range $FilteredMatches }} {{- end }}
Name Version Type Vulnerability Severity State Fixed In Description Related URLs PURL
{{.Artifact.Name}} {{.Artifact.Version}} {{.Artifact.Type}} {{.Vulnerability.ID}} {{.Vulnerability.Severity}} {{.Vulnerability.Fix.State}} {{- if .Vulnerability.Fix.Versions }}
    {{- range .Vulnerability.Fix.Versions }}
  • {{ . }}
  • {{- end }}
{{- else }} N/A {{- end }}
{{html .Vulnerability.Description}} {{ toJson .Vulnerability.URLs }} {{ .Artifact.PURL }}
================================================ FILE: templates/junit.tmpl ================================================ {{- $failures := len $.Matches }} {{- range .Matches }} {{- end }} ================================================ FILE: templates/markdown.tmpl ================================================ # Vulnerability Report - Name: {{- if eq (.Source.Type) "image" }} {{ .Source.Target.UserInput }} {{ else if eq (.Source.Type) "directory" }} {{ .Source.Target }} {{ else if eq (.Source.Type) "file" }} {{ .Source.Target }} {{ else if eq (.Source.Type) "sbom-file" }} {{ .Source.Target }} {{ else }} unknown {{ end -}} - Type: {{ .Source.Type }} {{- if eq .Source.Type "image" }} {{ with .Source.Target.ID }} - Checksum: {{ . }} {{- end -}} {{- end}} - Date: {{ .Descriptor.Timestamp }} | Package | Version Installed | Vulnerability ID | Severity | |---------|-------------------|------------------|----------| {{- range .Matches }} | {{ .Artifact.Name }} | {{ .Artifact.Version }} | {{ .Vulnerability.ID }} | {{ .Vulnerability.Severity }} | {{- end }} ================================================ FILE: templates/table.tmpl ================================================ {{- $name_length := 4}} {{- $installed_length := 9}} {{- $fixed_in_length := 8}} {{- $type_length := 4}} {{- $vulnerability_length := 13}} {{- $severity_length := 8}} {{- range .Matches}} {{- $temp_name_length := (len .Artifact.Name)}} {{- $temp_installed_length := (len .Artifact.Version)}} {{- $temp_fixed_in_length := (len (.Vulnerability.Fix.Versions | join " "))}} {{- $temp_type_length := (len .Artifact.Type)}} {{- $temp_vulnerability_length := (len .Vulnerability.ID)}} {{- $temp_severity_length := (len .Vulnerability.Severity)}} {{- if (lt $name_length $temp_name_length) }} {{- $name_length = $temp_name_length}} {{- end}} {{- if (lt $installed_length $temp_installed_length) }} {{- $installed_length = $temp_installed_length}} {{- end}} {{- if (lt $fixed_in_length $temp_fixed_in_length) }} {{- $fixed_in_length = $temp_fixed_in_length}} {{- end}} {{- if (lt $type_length $temp_type_length) }} {{- $type_length = $temp_type_length}} {{- end}} {{- if (lt $vulnerability_length $temp_vulnerability_length) }} {{- $vulnerability_length = $temp_vulnerability_length}} {{- end}} {{- if (lt $severity_length $temp_severity_length) }} {{- $severity_length = $temp_severity_length}} {{- end}} {{- end}} {{- $name_length = add $name_length 2}} {{- $pad_name := repeat (int $name_length) " "}} {{- $installed_length = add $installed_length 2}} {{- $pad_installed := repeat (int $installed_length) " "}} {{- $fixed_in_length = add $fixed_in_length 2}} {{- $pad_fixed_in := repeat (int $fixed_in_length) " "}} {{- $type_length = add $type_length 2}} {{- $pad_type := repeat (int $type_length) " "}} {{- $vulnerability_length = add $vulnerability_length 2}} {{- $pad_vulnerability := repeat (int $vulnerability_length) " "}} {{- $severity_length = add $severity_length 2}} {{- $pad_severity := repeat (int $severity_length) " "}} {{cat "NAME" (substr 5 (int $name_length) $pad_name)}}{{cat "INSTALLED" (substr 10 (int $installed_length) $pad_installed)}}{{cat "FIXED-IN" (substr 9 (int $fixed_in_length) $pad_fixed_in)}}{{cat "TYPE" (substr 5 (int $type_length) $pad_type)}}{{cat "VULNERABILITY" (substr 14 (int $vulnerability_length) $pad_vulnerability)}}{{cat "SEVERITY" (substr 9 (int $severity_length) $pad_severity)}} {{- range .Matches}} {{cat .Artifact.Name (substr (int (add (len .Artifact.Name) 1)) (int $name_length) $pad_name)}}{{cat .Artifact.Version (substr (int (add (len .Artifact.Version) 1)) (int $installed_length) $pad_installed)}}{{cat (.Vulnerability.Fix.Versions | join " ") (substr (int (add (len (.Vulnerability.Fix.Versions | join " ")) 1)) (int $fixed_in_length) $pad_fixed_in)}}{{cat .Artifact.Type (substr (int (add (len .Artifact.Type) 1)) (int $type_length) $pad_type)}}{{cat .Vulnerability.ID (substr (int (add (len .Vulnerability.ID) 1)) (int $vulnerability_length) $pad_vulnerability)}}{{cat .Vulnerability.Severity (substr (int (add (len .Vulnerability.Severity) 1)) (int $severity_length) $pad_severity)}} {{- end}} ================================================ FILE: test/cli/cmd_test.go ================================================ package cli import ( "encoding/json" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func TestCmd(t *testing.T) { tests := []struct { name string args []string env map[string]string assertions []traitAssertion }{ { name: "no-args-shows-help", args: []string{}, assertions: []traitAssertion{ assertInOutput("an image/directory argument is required"), // specific error that should be shown assertInOutput("A vulnerability scanner for container images, filesystems, and SBOMs"), // excerpt from help description assertFailingReturnCode, }, }, { name: "empty-string-arg-shows-help", args: []string{""}, assertions: []traitAssertion{ assertInOutput("an image/directory argument is required"), // specific error that should be shown assertInOutput("A vulnerability scanner for container images, filesystems, and SBOMs"), // excerpt from help description assertFailingReturnCode, }, }, { name: "ensure valid descriptor", args: []string{getFixtureImage(t, "image-bare"), "-o", "json"}, assertions: []traitAssertion{ assertInOutput(`"check-for-app-update":`), // assert existence of the app config block assertInOutput(`"db":`), // assert existence of the db status block assertInOutput(`"built":`), // assert existence of the db status block }, }, { name: "platform-option-wired-up", args: []string{"--platform", "arm64", "-o", "json", "registry:busybox:1.31"}, assertions: []traitAssertion{ assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest }, }, // TODO: uncomment this test when we can use `grype config` //{ // name: "responds-to-search-options", // args: []string{"--help"}, // env: map[string]string{ // "GRYPE_SEARCH_UNINDEXED_ARCHIVES": "true", // "GRYPE_SEARCH_INDEXED_ARCHIVES": "false", // "GRYPE_SEARCH_SCOPE": "all-layers", // }, // assertions: []traitAssertion{ // // the application config in the log matches that of what we expect to have been configured. Note: // // we are not testing further wiring of this option, only that the config responds to // // package-cataloger-level options. // assertInOutput("unindexed-archives: true"), // assertInOutput("indexed-archives: false"), // assertInOutput("scope: 'all-layers'"), // }, //}, { name: "vulnerabilities in output on -f with failure", args: []string{"registry:busybox:1.31", "-f", "high", "--platform", "linux/amd64"}, assertions: []traitAssertion{ assertInOutput("CVE-2021-42379"), assertFailingReturnCode, }, }, { name: "reason for ignored vulnerabilities is available in the template", args: []string{ "sbom:" + filepath.Join("testdata", "test-ignore-reason", "sbom.json"), "-c", filepath.Join("testdata", "test-ignore-reason", "config-with-ignore.yaml"), "-o", "template", "-t", filepath.Join("testdata", "test-ignore-reason", "template-with-ignore-reasons"), }, assertions: []traitAssertion{ assertInOutput("CVE-2021-42385 (test reason for vulnerability being ignored)"), assertSucceedingReturnCode, }, }, { name: "ignore-states wired up", args: []string{"./testdata/sbom-grype-source.json", "--ignore-states", "unknown"}, assertions: []traitAssertion{ assertSucceedingReturnCode, assertRowInStdOut([]string{"Pygments", "2.6.1", "2.7.4", "python", "GHSA-pq64-v7f5-gqh8", "High"}), assertNotInOutput("CVE-2014-6052"), }, }, { name: "ignore-states wired up - ignore fixed", args: []string{"./testdata/sbom-grype-source.json", "--ignore-states", "fixed"}, assertions: []traitAssertion{ assertSucceedingReturnCode, assertRowInStdOut([]string{"libvncserver", "0.9.9", "apk", "CVE-2014-6052", "High"}), assertNotInOutput("GHSA-pq64-v7f5-gqh8"), }, }, { name: "ignore-states wired up - ignore fixed, show suppressed", args: []string{"./testdata/sbom-grype-source.json", "--ignore-states", "fixed", "--show-suppressed"}, assertions: []traitAssertion{ assertSucceedingReturnCode, assertRowInStdOut([]string{"Pygments", "2.6.1", "2.7.4", "python", "GHSA-pq64-v7f5-gqh8", "High", "(suppressed)"}), }, }, { // from: https://github.com/anchore/grype/issues/2412 we need to ensure that explicit ignores in code don't break name: "explicit ignores wired up", args: []string{getFixtureImage(t, "image-java-subprocess")}, assertions: []traitAssertion{ assertSucceedingReturnCode, assertNotInOutput("CVE-2023-45853"), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd, stdout, stderr := runGrype(t, test.env, test.args...) for _, traitFn := range test.assertions { traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) } if t.Failed() { t.Log("STDOUT:\n", stdout) t.Log("STDERR:\n", stderr) t.Log("COMMAND:", strings.Join(cmd.Args, " ")) } }) } } func Test_descriptorNameAndVersionSet(t *testing.T) { _, output, _ := runGrype(t, nil, "-o", "json", getFixtureImage(t, "image-bare")) parsed := map[string]any{} err := json.Unmarshal([]byte(output), &parsed) require.NoError(t, err) desc, _ := parsed["descriptor"].(map[string]any) require.NotNil(t, desc) name := desc["name"] require.Equal(t, "grype", name) version := desc["version"] require.NotEmpty(t, version) } ================================================ FILE: test/cli/config_test.go ================================================ package cli import ( "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) func Test_configLoading(t *testing.T) { cwd, err := os.Getwd() require.NoError(t, err) configsDir := filepath.Join(cwd, "testdata", "configs") path := func(path string) string { return filepath.Join(configsDir, filepath.Join(strings.Split(path, "/")...)) } type ignore struct { Vuln string `yaml:"vulnerability"` } type config struct { Ignores []ignore `yaml:"ignore"` } tests := []struct { name string home string cwd string args []string expected []ignore err string }{ { name: "single explicit config", home: configsDir, cwd: cwd, args: []string{ "-c", path("dir1/.grype.yaml"), }, expected: []ignore{ { Vuln: "dir1-vuln1", }, { Vuln: "dir1-vuln2", }, }, }, { name: "multiple explicit config", home: configsDir, cwd: cwd, args: []string{ "-c", path("dir1/.grype.yaml"), "-c", path("dir2/.grype.yaml"), }, expected: []ignore{ { Vuln: "dir1-vuln1", }, { Vuln: "dir1-vuln2", }, { Vuln: "dir2-vuln1", }, { Vuln: "dir2-vuln2", }, }, }, { name: "empty profile override", home: configsDir, cwd: cwd, args: []string{ "-c", path("dir1/.grype.yaml"), "-c", path("dir2/.grype.yaml"), "--profile", "no-ignore", }, expected: []ignore{}, }, { name: "no profiles defined", home: configsDir, cwd: configsDir, args: []string{ "-c", path("dir3/.grype.yaml"), "--profile", "invalid", }, err: "not found in any configuration files", }, { name: "invalid profile name", home: configsDir, cwd: cwd, args: []string{ "-c", path("dir1/.grype.yaml"), "-c", path("dir2/.grype.yaml"), "--profile", "alt", }, err: "profile not found", }, { name: "explicit with profile override", home: configsDir, cwd: cwd, args: []string{ "-c", path("dir1/.grype.yaml"), "-c", path("dir2/.grype.yaml"), "--profile", "alt-ignore", }, expected: []ignore{ { Vuln: "dir1-alt-vuln1", // dir1 is still first }, { Vuln: "dir1-alt-vuln2", // dir1 is still first }, { Vuln: "dir2-alt-vuln1", }, { Vuln: "dir2-alt-vuln2", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Chdir(test.cwd) env := map[string]string{ "HOME": test.home, "XDG_CONFIG_HOME": test.home, } _, stdout, stderr := runGrype(t, env, append([]string{"config", "--load"}, test.args...)...) if test.err != "" { require.Contains(t, stderr, test.err) return } else { require.Empty(t, stderr) } got := config{} err = yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got) require.NoError(t, err) require.Equal(t, test.expected, got.Ignores) }) } } func Test_dpkgUseCPEsForEOLEnvVar(t *testing.T) { // Test that GRYPE_MATCH_DPKG_USE_CPES_FOR_EOL env var is properly wired up type matchConfig struct { Dpkg struct { UseCPEsForEOL bool `yaml:"use-cpes-for-eol"` } `yaml:"dpkg"` } type testConfig struct { Match matchConfig `yaml:"match"` } tests := []struct { name string envValue string expected bool }{ { name: "env var true enables CPE matching for EOL", envValue: "true", expected: true, }, { name: "env var false disables CPE matching for EOL", envValue: "false", expected: false, }, { name: "default is false", envValue: "", expected: false, }, } // Create a minimal config file for testing tmpDir := t.TempDir() cfgPath := filepath.Join(tmpDir, ".grype.yaml") err := os.WriteFile(cfgPath, []byte("check-for-app-update: false\n"), 0644) require.NoError(t, err) for _, test := range tests { t.Run(test.name, func(t *testing.T) { env := map[string]string{ "HOME": tmpDir, "XDG_CONFIG_HOME": tmpDir, } if test.envValue != "" { env["GRYPE_MATCH_DPKG_USE_CPES_FOR_EOL"] = test.envValue } _, stdout, stderr := runGrype(t, env, "-c", cfgPath, "config", "--load") require.Empty(t, stderr) got := testConfig{} err := yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got) require.NoError(t, err) assert.Equal(t, test.expected, got.Match.Dpkg.UseCPEsForEOL, "expected match.dpkg.use-cpes-for-eol to be %v", test.expected) }) } } func Test_rpmUseCPEsForEOLEnvVar(t *testing.T) { // Test that GRYPE_MATCH_RPM_USE_CPES_FOR_EOL env var is properly wired up type matchConfig struct { Rpm struct { UseCPEsForEOL bool `yaml:"use-cpes-for-eol"` } `yaml:"rpm"` } type testConfig struct { Match matchConfig `yaml:"match"` } tests := []struct { name string envValue string expected bool }{ { name: "env var true enables CPE matching for EOL", envValue: "true", expected: true, }, { name: "env var false disables CPE matching for EOL", envValue: "false", expected: false, }, { name: "default is false", envValue: "", expected: false, }, } // Create a minimal config file for testing tmpDir := t.TempDir() cfgPath := filepath.Join(tmpDir, ".grype.yaml") err := os.WriteFile(cfgPath, []byte("check-for-app-update: false\n"), 0644) require.NoError(t, err) for _, test := range tests { t.Run(test.name, func(t *testing.T) { env := map[string]string{ "HOME": tmpDir, "XDG_CONFIG_HOME": tmpDir, } if test.envValue != "" { env["GRYPE_MATCH_RPM_USE_CPES_FOR_EOL"] = test.envValue } _, stdout, stderr := runGrype(t, env, "-c", cfgPath, "config", "--load") require.Empty(t, stderr) got := testConfig{} err := yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got) require.NoError(t, err) assert.Equal(t, test.expected, got.Match.Rpm.UseCPEsForEOL, "expected match.rpm.use-cpes-for-eol to be %v", test.expected) }) } } ================================================ FILE: test/cli/db_providers_test.go ================================================ package cli import ( "strings" "testing" ) func TestDBProviders(t *testing.T) { tests := []struct { name string args []string env map[string]string assertions []traitAssertion }{ { name: "db providers command", args: []string{"db", "providers"}, assertions: []traitAssertion{ assertNoStderr, assertDbProvidersTableReport, }, }, { name: "db providers command help", args: []string{"db", "providers", "-h"}, assertions: []traitAssertion{ assertInOutput("List vulnerability providers that are in the database"), assertNoStderr, }, }, { name: "db providers command with table output flag", args: []string{"db", "providers", "-o", "table"}, assertions: []traitAssertion{ assertNoStderr, assertDbProvidersTableReport, }, }, { name: "db providers command with json output flag", args: []string{"db", "providers", "-o", "json"}, assertions: []traitAssertion{ assertInOutput("processor"), assertNoStderr, assertJsonReport, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd, stdout, stderr := runGrype(t, test.env, test.args...) for _, traitAssertionFn := range test.assertions { traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) } if t.Failed() { t.Log("STDOUT:\n", stdout) t.Log("STDERR:\n", stderr) t.Log("COMMAND:", strings.Join(cmd.Args, " ")) } }) } } ================================================ FILE: test/cli/db_validations_test.go ================================================ package cli import ( "encoding/json" "fmt" "net" "net/http" "os" "path/filepath" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/require" v6 "github.com/anchore/grype/grype/db/v6" "github.com/anchore/grype/internal/dbtest" "github.com/anchore/grype/internal/schemaver" ) func TestDBValidations(t *testing.T) { invalidUpdateURL := fmt.Sprintf("https://localhost:%v", availablePort()) expiredDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24*24*time.Hour)).SetDBVersion(6, 0, 0).Start() // 24 days old yesterdayDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24*time.Hour)).SetDBVersion(6, 0, 0).Start() // 24 hours old todayDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(6, 0, 0).Start() // just built todayDbNewerVersionURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(6, 0, 1).Start() // just built todayDbOlderVersionURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(5, 9, 9).Start() // just built notFoundDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(3 * time.Hour)).WithHandler(http.NotFound).Start() // common setup functions type setupFunc = func(t *testing.T, dir string) setup := func(funcs ...setupFunc) setupFunc { return func(t *testing.T, dir string) { for _, f := range funcs { f(t, dir) } } } setupDb := func(url string) setupFunc { return func(t *testing.T, dir string) { cmd, stdout, stderr := runGrype(t, map[string]string{ "GRYPE_DB_CACHE_DIR": dir, "GRYPE_DB_UPDATE_URL": url, }, "db", "update", "-vvv") assertInOutput("downloading new vulnerability DB")(t, stdout, stderr, cmd.ProcessState.ExitCode()) assertSucceedingReturnCode(t, stdout, stderr, cmd.ProcessState.ExitCode()) } } setupExpiredDb := setupDb(expiredDbURL) setupYesterdayDb := setupDb(yesterdayDbURL) setupTodayDb := setupDb(todayDbURL) dbFilePath := func(dir string) string { return filepath.Join(dir, strconv.Itoa(v6.ModelVersion), "vulnerability.db") } corruptDb := func(t *testing.T, dir string) { err := os.Truncate(dbFilePath(dir), 20) require.NoError(t, err) } moveDbToBackup := func(t *testing.T, dir string) { err := os.Rename(dbFilePath(dir), filepath.Join(dir, "db.old")) require.NoError(t, err) } restoreDbFromBackup := func(t *testing.T, dir string) { // replace with valid db, which doesn't match the hash err := os.Rename(filepath.Join(dir, "db.old"), dbFilePath(dir)) require.NoError(t, err) } deleteDb := func(t *testing.T, dir string) { err := os.Remove(dbFilePath(dir)) require.NoError(t, err) } // common asserts assertDbDownloaded := assertInOutput("downloading new vulnerability DB") assertDbNotDownloaded := assertNotInOutput("downloading new vulnerability DB") assertScanRan := assertInOutput("No vulnerabilities found") assertDbLoadFailed := assertInOutput("failed to load vulnerability db") assertDbLoadNotAtempted := assertNotInOutput("failed to load vulnerability db") assertDbNotFound := assertInOutput("No installed DB version found") assertCheckedForDbUpdate := assertInOutput("checking for available database updates") assertDbHashed := assertInOutput("captured DB digest") assertUpdateMessageDisplayed := assertInOutput("update to the latest db") cmdAliases := map[string]string{"scan": "pkg:no/thing@0"} // scan: matching a purl with no vulnerabilities // ensure we have grype built and ready runGrype(t, map[string]string{}, "config") tests := []struct { name string // the portion of the name before `:` is the command to run from cmdAliases above or the literal value setup setupFunc // setup to run before test cmd dbUpdateURL string // update url to use, e.g. todayDbURL dbRequireUpdate bool // whether an update check is required dbMaxUpdateCheckFrequency string // max update check frequency, defaults to 0 to always check dbValidateHash bool // whether to validate existing db by hash dbValidateAge bool // whether to validate existing db age dbCaCert string // ca cert file, if set assertions []traitAssertion }{ { name: "scan: new install downloads successfully", setup: nil, dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "scan: existing db updates successfully", setup: setupYesterdayDb, dbUpdateURL: todayDbURL, assertions: []traitAssertion{ assertDbHashed, assertDbDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "scan: existing db skips update when same", setup: setupYesterdayDb, dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbNotDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "scan: existing db skips update when newer", setup: setupTodayDb, dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbNotDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "scan: continues on corrupt db no update", setup: setup(setupYesterdayDb, corruptDb), dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "db check: continues on corrupt db no update", setup: setup(setupYesterdayDb, corruptDb), dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbNotFound, assertFailingReturnCode, }, }, { name: "db check: continues on corrupt db with update", setup: setup(setupYesterdayDb, corruptDb), dbUpdateURL: todayDbURL, assertions: []traitAssertion{ assertDbNotFound, assertFailingReturnCode, }, }, { name: "db status: fails with corrupt db no update", setup: setup(setupYesterdayDb, corruptDb), dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbNotDownloaded, assertInOutput("failed to read DB description"), assertFailingReturnCode, }, }, { name: "db status: fails with corrupt db with update", setup: setup(setupYesterdayDb, corruptDb), dbUpdateURL: todayDbURL, assertions: []traitAssertion{ assertDbNotDownloaded, assertInOutput("failed to read DB description"), assertFailingReturnCode, }, }, { name: "scan: missing db downloads a new one", setup: setup(setupYesterdayDb, deleteDb), dbUpdateURL: todayDbURL, assertions: []traitAssertion{ assertDbDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "db check: missing db does not affect no update", setup: setup(setupYesterdayDb, deleteDb), dbUpdateURL: yesterdayDbURL, assertions: []traitAssertion{ assertDbNotFound, assertFailingReturnCode, }, }, { name: "db check: missing db does not affect with update", setup: setup(setupYesterdayDb, deleteDb), dbUpdateURL: todayDbURL, assertions: []traitAssertion{ assertDbNotFound, assertFailingReturnCode, }, }, { name: "db status: missing db returns error", setup: setup(setupYesterdayDb, deleteDb), dbUpdateURL: todayDbURL, assertions: []traitAssertion{ assertInOutput("database does not exist"), assertFailingReturnCode, }, }, { name: "db status: valid db fails with hash mismatch", setup: setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup), dbUpdateURL: invalidUpdateURL, dbValidateHash: true, assertions: []traitAssertion{ assertInOutput("bad db checksum"), assertFailingReturnCode, }, }, { name: "db check: valid db with hash mismatch", setup: setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup), dbUpdateURL: invalidUpdateURL, dbValidateHash: true, assertions: []traitAssertion{ assertDbLoadNotAtempted, assertFailingReturnCode, }, }, { name: "scan: valid db fails with hash mismatch", setup: setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup), dbUpdateURL: invalidUpdateURL, dbValidateHash: true, assertions: []traitAssertion{ assertInOutput("bad db checksum"), assertDbLoadFailed, assertDbNotDownloaded, // notification mentions grype db delete and grype db update assertInOutput("grype db delete"), assertInOutput("grype db update"), assertFailingReturnCode, }, }, { name: "scan: missing import.json", setup: setup(setupYesterdayDb, func(t *testing.T, dir string) { require.NoError(t, os.Remove(filepath.Join(filepath.Dir(dbFilePath(dir)), "import.json"))) }), dbUpdateURL: invalidUpdateURL, dbValidateHash: true, assertions: []traitAssertion{ assertInOutput("no import metadata"), assertDbLoadFailed, assertDbNotDownloaded, // notification mentions grype db delete and grype db update assertInOutput("grype db delete"), assertInOutput("grype db update"), assertFailingReturnCode, }, }, { name: "scan: update check error with valid db continues", setup: setupYesterdayDb, dbUpdateURL: notFoundDbURL, dbRequireUpdate: false, assertions: []traitAssertion{ assertInOutput("error updating db"), assertSucceedingReturnCode, }, }, { name: "scan: update check error with valid db fails when require update", setup: setupYesterdayDb, dbUpdateURL: notFoundDbURL, dbRequireUpdate: true, assertions: []traitAssertion{ assertInOutput("unable to update db"), assertFailingReturnCode, }, }, { name: "db check: update check error with valid db fails", setup: setupYesterdayDb, dbUpdateURL: notFoundDbURL, dbRequireUpdate: false, assertions: []traitAssertion{ assertInOutput("unable to check for vulnerability database update"), assertFailingReturnCode, }, }, { name: "scan: database older than max age fails when unable to update", setup: setupExpiredDb, dbUpdateURL: notFoundDbURL, dbValidateAge: true, assertions: []traitAssertion{ assertInOutput("the vulnerability database was built"), assertFailingReturnCode, }, }, { name: "scan: database older than max age succeeds with update", setup: setupExpiredDb, dbUpdateURL: todayDbURL, dbValidateAge: true, assertions: []traitAssertion{ assertDbDownloaded, assertScanRan, assertSucceedingReturnCode, }, }, { name: "scan: no panic on bad cert configuration", dbCaCert: "./does-not-exist.crt", assertions: []traitAssertion{ assertInOutput("failed to load vulnerability db"), assertFailingReturnCode, }, }, { name: "db check: always check for updates regardless of frequency", setup: setupYesterdayDb, dbUpdateURL: todayDbURL, dbMaxUpdateCheckFrequency: "10h", assertions: []traitAssertion{ assertCheckedForDbUpdate, assertUpdateMessageDisplayed, func(tb testing.TB, stdout, stderr string, rc int) { require.Equal(t, 100, rc) }, }, }, { name: "db update: always update regardless of frequency", setup: setupYesterdayDb, dbUpdateURL: todayDbURL, dbMaxUpdateCheckFrequency: "10h", assertions: []traitAssertion{ assertCheckedForDbUpdate, assertDbDownloaded, assertSucceedingReturnCode, }, }, { name: "scan: ensure db update frequency config is respected", setup: setupYesterdayDb, dbUpdateURL: todayDbURL, dbMaxUpdateCheckFrequency: "10h", // last check was during setup, much more recently than 10h ago assertions: []traitAssertion{ assertNotInOutput("no max-frequency set for update check"), assertNotInOutput("checking for available database updates"), assertDbNotDownloaded, assertInOutput("max-update-check-frequency: 10h"), assertSucceedingReturnCode, }, }, { name: "scan: ensure newer db version with older grype is not hydrated", setup: setup(setupDb(todayDbNewerVersionURL), func(t *testing.T, dir string) { // change the hydration version to a newer version that this grype metaFile := filepath.Join(filepath.Dir(dbFilePath(dir)), v6.ImportMetadataFileName) contents, err := os.ReadFile(metaFile) require.NoError(t, err) meta := v6.ImportMetadata{} err = json.Unmarshal(contents, &meta) require.NoError(t, err) meta.ClientVersion = schemaver.New(v6.ModelVersion, v6.Revision, v6.Addition+1).String() contents, err = json.Marshal(meta) require.NoError(t, err) err = os.WriteFile(metaFile, contents, 0x777) require.NoError(t, err) }), dbUpdateURL: todayDbNewerVersionURL, assertions: []traitAssertion{ assertInOutput("DB rehydration not needed"), assertDbNotDownloaded, assertSucceedingReturnCode, }, }, { name: "scan: ensure older db version with newer db version hydrated", setup: setup(setupDb(todayDbNewerVersionURL), func(t *testing.T, dir string) { // change the hydration version to a older version that this grype metaFile := filepath.Join(filepath.Dir(dbFilePath(dir)), v6.ImportMetadataFileName) contents, err := os.ReadFile(metaFile) require.NoError(t, err) meta := v6.ImportMetadata{} err = json.Unmarshal(contents, &meta) require.NoError(t, err) meta.ClientVersion = schemaver.New(v6.ModelVersion-1, v6.Revision, v6.Addition).String() contents, err = json.Marshal(meta) require.NoError(t, err) err = os.WriteFile(metaFile, contents, 0x777) require.NoError(t, err) }), dbUpdateURL: todayDbOlderVersionURL, assertions: []traitAssertion{ assertInOutput("rehydrating DB"), assertDbNotDownloaded, assertSucceedingReturnCode, }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() dbDir := t.TempDir() if test.setup != nil { test.setup(t, dbDir) } // set up values env := map[string]string{ "GRYPE_DB_CACHE_DIR": dbDir, "GRYPE_DB_UPDATE_URL": defaultValue(test.dbUpdateURL, invalidUpdateURL), "GRYPE_DB_VALIDATE_BY_HASH_ON_START": fmt.Sprintf("%v", defaultValue(test.dbValidateHash, false)), "GRYPE_DB_VALIDATE_AGE": fmt.Sprintf("%v", defaultValue(test.dbValidateAge, false)), "GRYPE_DB_MAX_UPDATE_CHECK_FREQUENCY": defaultValue(test.dbMaxUpdateCheckFrequency, "0"), } if test.dbValidateAge { env["GRYPE_DB_MAX_ALLOWED_BUILT_AGE"] = "48h" // expired db is 24 days old } if test.dbCaCert != "" { env["GRYPE_DB_CA_CERT"] = test.dbCaCert } if test.dbRequireUpdate { env["GRYPE_DB_REQUIRE_UPDATE_CHECK"] = "true" } // test name before : is command args args := strings.Split(test.name, ":") args = strings.Split(args[0], " ") if cmd := cmdAliases[args[0]]; cmd != "" { args[0] = cmd } cmd, stdout, stderr := runGrype(t, env, append(args, "-vvv")...) for _, traitAssertionFn := range test.assertions { traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) } if t.Failed() { t.Log("STDOUT:\n", stdout) t.Log("STDERR:\n", stderr) t.Log("COMMAND:", strings.Join(cmd.Args, " ")) } }) } } func defaultValue[T comparable](value T, defaultValue T) T { var empty T if value == empty { return defaultValue } return value } func availablePort() int { if a, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0"); err == nil { var l *net.TCPListener if l, err = net.ListenTCP("tcp", a); err == nil { defer func() { _ = l.Close() }() return l.Addr().(*net.TCPAddr).Port } } panic("unable to get port") } ================================================ FILE: test/cli/registry_auth_test.go ================================================ package cli import ( "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func TestRegistryAuth(t *testing.T) { tests := []struct { name string args []string env map[string]string assertions []traitAssertion }{ { name: "fallback to keychain", args: []string{"-vv", "registry:localhost:5000/something:latest"}, assertions: []traitAssertion{ assertInOutput("from registry"), assertInOutput("localhost:5000/something:latest"), assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`), }, }, { name: "use creds", args: []string{"-vv", "registry:localhost:5000/something:latest"}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_AUTHORITY": "localhost:5000", "GRYPE_REGISTRY_AUTH_USERNAME": "username", "GRYPE_REGISTRY_AUTH_PASSWORD": "password", }, assertions: []traitAssertion{ assertInOutput("from registry"), assertInOutput("localhost:5000/something:latest"), assertInOutput(`using basic auth for registry "localhost:5000"`), }, }, { name: "use token", args: []string{"-vv", "registry:localhost:5000/something:latest"}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_AUTHORITY": "localhost:5000", "GRYPE_REGISTRY_AUTH_TOKEN": "my-token", }, assertions: []traitAssertion{ assertInOutput("from registry"), assertInOutput("localhost:5000/something:latest"), assertInOutput(`using token for registry "localhost:5000"`), }, }, { name: "not enough info fallsback to keychain", args: []string{"-vv", "registry:localhost:5000/something:latest"}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_AUTHORITY": "localhost:5000", }, assertions: []traitAssertion{ assertInOutput("from registry"), assertInOutput("localhost:5000/something:latest"), assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`), }, }, { name: "allows insecure http flag", args: []string{"-vv", "registry:localhost:5000/something:latest"}, env: map[string]string{ "GRYPE_REGISTRY_INSECURE_USE_HTTP": "true", }, assertions: []traitAssertion{ assertInOutput("insecure-use-http: true"), }, }, { name: "use tls configuration", args: []string{"-vvv", "registry:localhost:5000/something:latest"}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_TLS_CERT": "place.crt", "GRYPE_REGISTRY_AUTH_TLS_KEY": "place.key", }, assertions: []traitAssertion{ assertInOutput("using custom TLS credentials from"), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cmd, stdout, stderr := runGrype(t, test.env, test.args...) for _, traitAssertionFn := range test.assertions { traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) } if t.Failed() { t.Log("STDOUT:\n", stdout) t.Log("STDERR:\n", stderr) t.Log("COMMAND:", strings.Join(cmd.Args, " ")) } }) } } func TestRegistryAuthRedactions(t *testing.T) { tmp := filepath.Join(t.TempDir(), "output.json") assertNotInFile := func(text string) traitAssertion { return func(tb testing.TB, stdout, stderr string, rc int) { contents, err := os.ReadFile(tmp) require.NoError(tb, err) require.NotEmpty(tb, contents) require.NotContains(tb, string(contents), text) } } tests := []struct { name string args []string env map[string]string assertions []traitAssertion }{ { name: "use creds", args: []string{"-vv", "sbom:testdata/sbom-grype-source.json", "-o", "json"}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_USERNAME": "foobar-username", "GRYPE_REGISTRY_AUTH_PASSWORD": "foobar-password", }, assertions: []traitAssertion{ assertSucceedingReturnCode, assertNotInOutput("foobar-username"), assertNotInOutput("foobar-password"), }, }, { name: "use token", args: []string{"-vv", "sbom:testdata/sbom-grype-source.json", "-o", "json"}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_TOKEN": "foobar-token", }, assertions: []traitAssertion{ assertSucceedingReturnCode, assertNotInOutput("foobar-token"), }, }, { name: "use creds file", args: []string{"-vv", "sbom:testdata/sbom-grype-source.json", "-o", "json", "--file", tmp}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_USERNAME": "foobar-username", "GRYPE_REGISTRY_AUTH_PASSWORD": "foobar-password", }, assertions: []traitAssertion{ assertSucceedingReturnCode, assertNotInFile("foobar-username"), assertNotInFile("foobar-password"), assertNotInOutput("foobar-username"), assertNotInOutput("foobar-password"), }, }, { name: "use token file", args: []string{"-vv", "sbom:testdata/sbom-grype-source.json", "-o", "json", "--file", tmp}, env: map[string]string{ "GRYPE_REGISTRY_AUTH_TOKEN": "foobar-token", }, assertions: []traitAssertion{ assertSucceedingReturnCode, assertNotInFile("foobar-token"), assertNotInOutput("foobar-token"), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _ = os.Remove(tmp) // ok to fail cmd, stdout, stderr := runGrype(t, test.env, test.args...) for _, traitAssertionFn := range test.assertions { traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) } if t.Failed() { fileContents, _ := os.ReadFile(tmp) t.Log("FILE:\n", string(fileContents)) t.Log("STDOUT:\n", stdout) t.Log("STDERR:\n", stderr) t.Log("COMMAND:", strings.Join(cmd.Args, " ")) } }) } } ================================================ FILE: test/cli/sbom_input_test.go ================================================ package cli import ( "os" "os/exec" "path" "runtime" "testing" "github.com/stretchr/testify/require" ) func TestSBOMInput_AsArgument(t *testing.T) { workingDirectory, err := os.Getwd() if err != nil { t.Fatal(err) } cases := []struct { name string path string }{ { "absolute path - image scan", path.Join(workingDirectory, "./testdata/sbom-ubuntu-20.04--pruned.json"), }, { "relative path - image scan", "./testdata/sbom-ubuntu-20.04--pruned.json", }, { "directory scan", "./testdata/sbom-grype-source.json", }, } t.Run("explicit", func(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { sbomArg := "sbom:" + tc.path cmd := getGrypeCommand(t, sbomArg) assertCommandExecutionSuccess(t, cmd) }) } }) t.Run("implicit", func(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { sbomArg := tc.path cmd := getGrypeCommand(t, sbomArg) assertCommandExecutionSuccess(t, cmd) }) } }) } func TestSBOMInput_FromStdin(t *testing.T) { tests := []struct { name string input string args []string wantErr require.ErrorAssertionFunc wantOutput string }{ { name: "empty file", input: "./testdata/empty.json", args: []string{"-c", "../grype-test-config.yaml"}, wantErr: require.Error, wantOutput: "unable to decode sbom: sbom format not recognized", }, { name: "sbom", input: "./testdata/sbom-ubuntu-20.04--pruned.json", args: []string{"-c", "../grype-test-config.yaml"}, wantErr: require.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := exec.Command(getGrypeSnapshotLocation(t, runtime.GOOS), tt.args...) input, err := os.Open(tt.input) require.NoError(t, err) attachFileToCommandStdin(t, input, cmd) err = input.Close() require.NoError(t, err) output, err := cmd.CombinedOutput() tt.wantErr(t, err, "output: %s", output) if tt.wantOutput != "" { require.Contains(t, string(output), tt.wantOutput) } }) } } ================================================ FILE: test/cli/subprocess_test.go ================================================ package cli import ( "fmt" "path" "strings" "testing" "time" "github.com/anchore/stereoscope/pkg/imagetest" ) func TestSubprocessStdin(t *testing.T) { binDir := path.Dir(getGrypeSnapshotLocation(t, "linux")) tests := []struct { name string args []string env map[string]string assertions []traitAssertion }{ { // regression name: "ensure can be used by node subprocess (without hanging)", args: []string{"-v", fmt.Sprintf("%s:%s:ro", binDir, "/app/bin"), imagetest.LoadFixtureImageIntoDocker(t, "image-node-subprocess"), "node", "/app.js"}, env: map[string]string{ "GRYPE_CHECK_FOR_APP_UPDATE": "false", }, assertions: []traitAssertion{ assertSucceedingReturnCode, }, }, { // regression: https://github.com/anchore/grype/issues/267 name: "ensure can be used by java subprocess (without hanging)", args: []string{"-v", fmt.Sprintf("%s:%s:ro", binDir, "/app/bin"), imagetest.LoadFixtureImageIntoDocker(t, "image-java-subprocess"), "java", "/app.java"}, env: map[string]string{ "GRYPE_CHECK_FOR_APP_UPDATE": "false", }, assertions: []traitAssertion{ assertSucceedingReturnCode, }, }, } for _, test := range tests { testFn := func(t *testing.T) { cmd := getDockerRunCommand(t, test.args...) stdout, stderr := runCommand(cmd, test.env) for _, traitAssertionFn := range test.assertions { traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode()) } if t.Failed() { t.Log("STDOUT:\n", stdout) t.Log("STDERR:\n", stderr) t.Log("COMMAND:", strings.Join(cmd.Args, " ")) } } testWithTimeout(t, test.name, 60*time.Second, testFn) } } ================================================ FILE: test/cli/testdata/Makefile ================================================ # change these if you want CI to not use previous stored cache CLI_CACHE_BUSTER := "e5cdfd8" .PHONY: cache.fingerprint cache.fingerprint: find image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> cache.fingerprint ================================================ FILE: test/cli/testdata/another_cosign.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFkdSiEHXuVIQMJWLeRbj+xuAIdzB YNPLm67ahl7GBcPYWirZfssklnwDldY8TLbK4igxT7YisGPMLGyiJsvvhg== -----END PUBLIC KEY----- ================================================ FILE: test/cli/testdata/configs/dir1/.grype.yaml ================================================ ignore: - vulnerability: 'dir1-vuln1' - vulnerability: 'dir1-vuln2' profiles: no-ignore: ignore: [] alt-ignore: ignore: - vulnerability: 'dir1-alt-vuln1' - vulnerability: 'dir1-alt-vuln2' ================================================ FILE: test/cli/testdata/configs/dir2/.grype.yaml ================================================ ignore: - vulnerability: 'dir2-vuln1' - vulnerability: 'dir2-vuln2' profiles: alt-ignore: ignore: - vulnerability: 'dir2-alt-vuln1' - vulnerability: 'dir2-alt-vuln2' ================================================ FILE: test/cli/testdata/configs/dir3/.grype.yaml ================================================ ================================================ FILE: test/cli/testdata/cosign.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB ZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg== -----END PUBLIC KEY----- ================================================ FILE: test/cli/testdata/cosign_broken.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB ZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg=1 -----END PUBLIC KEY----- ================================================ FILE: test/cli/testdata/empty.json ================================================ ================================================ FILE: test/cli/testdata/image-bare/Dockerfile ================================================ FROM scratch ADD file-1.txt . ================================================ FILE: test/cli/testdata/image-bare/file-1.txt ================================================ this file has contents ================================================ FILE: test/cli/testdata/image-java-subprocess/Dockerfile ================================================ FROM openjdk:15-slim-buster@sha256:1e069bf1c5c23adde58b29b82281b862e473d698ce7cc4e164194a0a2a1c044a COPY app.java / ENV PATH="/app/bin:${PATH}" WORKDIR / ================================================ FILE: test/cli/testdata/image-java-subprocess/app.java ================================================ import java.io.IOException; public class GrypeExecutionTest { public static void main(String[] args) { try { ProcessBuilder builder = new ProcessBuilder("grype", "registry:busybox:latest", "-vv"); builder.inheritIO(); Process process = builder.start(); process.waitFor(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } ================================================ FILE: test/cli/testdata/image-node-subprocess/Dockerfile ================================================ FROM node:16-stretch@sha256:5810de52349af302a2c5dddf0a3f31174ef65d0eed8985959a5e83bb1084b79b COPY app.js / ENV PATH="/app/bin:${PATH}" WORKDIR / ================================================ FILE: test/cli/testdata/image-node-subprocess/app.js ================================================ require("child_process").spawn("grype", [ "-vv", "registry:busybox:latest", ], { // we want to see any output from stdout/stderr which is why they are inherited from the parent process. // The real test is to make certain that piped input will not hang forever when nothing is provided on stdin // and there is input from the user to not use stdin. That is --make certain that we don't use "stdin is a pipe" // as the only indicator to expect analysis input from stdin. stdio: ["pipe", "inherit", "inherit"] }); ================================================ FILE: test/cli/testdata/sbom-grype-source.json ================================================ { "artifacts": [ { "id": "bef1ce7f-cce6-4049-9da4-53882a612bb3", "name": "Pygments", "version": "2.6.1", "type": "python", "foundBy": "python-package-cataloger", "locations": [ { "path": "test/integration/testdata/image-debian-match-coverage/python/dist-info/METADATA" }, { "path": "test/integration/testdata/image-debian-match-coverage/python/dist-info/top_level.txt" } ], "licenses": [ "BSD License" ], "language": "python", "cpes": [ "cpe:2.3:a:python-Pygments:python-Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python_Pygments:python-Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python-Pygments:python_Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python_Pygments:python_Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:georg_brandl:python_Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:georg_brandl:python-Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:Pygments:python_Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:Pygments:python-Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python_Pygments:Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python-Pygments:Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python:python_Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python:python-Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:georg_brandl:Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:georg:python-Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:georg:python_Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:Pygments:Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:python:Pygments:2.6.1:*:*:*:*:*:*:*", "cpe:2.3:a:georg:Pygments:2.6.1:*:*:*:*:*:*:*" ], "purl": "pkg:pypi/Pygments@2.6.1", "metadataType": "PythonPackageMetadata", "metadata": { "name": "Pygments", "version": "2.6.1", "license": "BSD License", "author": "Georg Brandl", "authorEmail": "georg@python.org", "platform": "any", "sitePackagesRootPath": "test/integration/testdata/image-debian-match-coverage/python", "topLevelPackages": [ "pygments" ] } }, { "id": "651f06ac-d509-4b33-92f1-88bf006beaf7", "name": "apt", "version": "1.8.2", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "test/integration/testdata/image-debian-match-coverage/var/lib/dpkg/status" } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:apt:apt:1.8.2:*:*:*:*:*:*:*" ], "purl": "", "metadataType": "DpkgMetadata", "metadata": { "package": "apt", "source": "apt-dev", "version": "1.8.2", "sourceVersion": "", "architecture": "amd64", "maintainer": "APT Development Team ", "installedSize": 4064, "files": [ { "path": "/etc/apt/apt.conf.d/01autoremove", "digest": { "algorithm": "md5", "value": "76120d358bc9037bb6358e737b3050b5" }, "isConfigFile": true }, { "path": "/etc/cron.daily/apt-compat", "digest": { "algorithm": "md5", "value": "49e9b2cfa17849700d4db735d04244f3" }, "isConfigFile": true }, { "path": "/etc/kernel/postinst.d/apt-auto-removal", "digest": { "algorithm": "md5", "value": "4ad976a68f045517cf4696cec7b8aa3a" }, "isConfigFile": true }, { "path": "/etc/logrotate.d/apt", "digest": { "algorithm": "md5", "value": "179f2ed4f85cbaca12fa3d69c2a4a1c3" }, "isConfigFile": true } ] } }, { "id": "e1d0474e-2a82-420c-ad82-a9de40d866c7", "name": "dive", "version": "0.9.2-1", "type": "rpm", "foundBy": "rpm-db-cataloger", "locations": [ { "path": "test/integration/testdata/image-sles-match-coverage/var/lib/rpm/Packages" } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:dive:dive:0.9.2-1:*:*:*:*:*:*:*" ], "purl": "", "metadataType": "RpmMetadata", "metadata": { "name": "dive", "version": "0.9.2", "epoch": null, "architecture": "x86_64", "release": "1", "sourceRpm": "dive-0.9.2-1.src.rpm", "size": 12406784, "license": "MIT", "vendor": "", "files": [] } }, { "id": "4f86c82e-2e7a-4f61-97cc-2058938257be", "name": "example-java-app-maven", "version": "0.1.0", "type": "java-archive", "foundBy": "java-cataloger", "locations": [ { "path": "test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar" } ], "licenses": [], "language": "java", "cpes": [ "cpe:2.3:a:example_java_app_maven:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example_java_app_maven:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example-java-app-maven:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example-java-app-maven:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example_java_app:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example_java_app:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example-java-app:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example-java-app:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example_java:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example-java:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example-java:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example_java:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:example:example_java_app_maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:anchore:example-java-app-maven:0.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:anchore:example_java_app_maven:0.1.0:*:*:*:*:*:*:*" ], "purl": "pkg:maven/org.anchore/example-java-app-maven@0.1.0", "metadataType": "JavaMetadata", "metadata": { "virtualPath": "test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar", "manifest": { "main": { "Archiver-Version": "Plexus Archiver", "Build-Jdk": "14.0.1", "Built-By": "?", "Created-By": "Apache Maven 3.6.3", "Main-Class": "hello.HelloWorld", "Manifest-Version": "1.0" } }, "pomProperties": { "path": "META-INF/maven/org.anchore/example-java-app-maven/pom.properties", "name": "", "groupId": "org.anchore", "artifactId": "example-java-app-maven", "version": "0.1.0", "extraFields": {} } } }, { "id": "26f1dd60-f006-480f-8ad2-3c92a8d19f42", "name": "github.com/acarl005/stripansi", "version": "v0.0.0-20180116102854-5a71ef0e047d", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:acarl005:stripansi:v0.0.0-20180116102854-5a71ef0e047d:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/acarl005/stripansi@v0.0.0-20180116102854-5a71ef0e047d", "metadataType": "", "metadata": null }, { "id": "b296ad62-b21d-487e-9914-6a5f5358f53a", "name": "github.com/adrg/xdg", "version": "v0.2.1", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:adrg:xdg:v0.2.1:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/adrg/xdg@v0.2.1", "metadataType": "", "metadata": null }, { "id": "e1a7a167-c7c1-41b8-938e-3b17de0907fd", "name": "github.com/anchore/go-testutils", "version": "v0.0.0-20200925183923-d5f45b0d3c04", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:anchore:go_testutils:v0.0.0-20200925183923-d5f45b0d3c04:*:*:*:*:*:*:*", "cpe:2.3:a:anchore:go-testutils:v0.0.0-20200925183923-d5f45b0d3c04:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/anchore/go-testutils@v0.0.0-20200925183923-d5f45b0d3c04", "metadataType": "", "metadata": null }, { "id": "9ae8c8cb-0513-46c4-9a10-91720a0cf3c0", "name": "github.com/anchore/go-version", "version": "v1.2.2-0.20210903204242-51efa5b487c4", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:anchore:go-version:v1.2.2-0.20210903204242-51efa5b487c4:*:*:*:*:*:*:*", "cpe:2.3:a:anchore:go_version:v1.2.2-0.20210903204242-51efa5b487c4:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/anchore/go-version@v1.2.2-0.20210903204242-51efa5b487c4", "metadataType": "", "metadata": null }, { "id": "2268071f-cb45-4f99-82d3-87ac2ad0f1cc", "name": "github.com/anchore/grype-db", "version": "v0.0.0-20210928194208-f146397d6cd0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:anchore:grype-db:v0.0.0-20210928194208-f146397d6cd0:*:*:*:*:*:*:*", "cpe:2.3:a:anchore:grype_db:v0.0.0-20210928194208-f146397d6cd0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/anchore/grype-db@v0.0.0-20210928194208-f146397d6cd0", "metadataType": "", "metadata": null }, { "id": "65c50f92-183e-4ff5-ba28-15c7eb5838dc", "name": "github.com/anchore/stereoscope", "version": "v0.0.0-20210817160504-0f4abc2a5a5a", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:anchore:stereoscope:v0.0.0-20210817160504-0f4abc2a5a5a:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/anchore/stereoscope@v0.0.0-20210817160504-0f4abc2a5a5a", "metadataType": "", "metadata": null }, { "id": "42c879dc-c9d8-4c08-afde-2623f0ce6749", "name": "github.com/anchore/syft", "version": "v0.24.1", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:anchore:syft:v0.24.1:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/anchore/syft@v0.24.1", "metadataType": "", "metadata": null }, { "id": "70be86c2-9e8b-43b3-90d7-2f0e61100b54", "name": "github.com/bmatcuk/doublestar/v2", "version": "v2.0.4", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:bmatcuk:doublestar:v2.0.4:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/bmatcuk/doublestar/v2@v2.0.4", "metadataType": "", "metadata": null }, { "id": "814fe4dd-afad-4db8-bb2a-09d9d8fccbb1", "name": "github.com/docker/docker", "version": "v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:docker:docker:v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/docker/docker@v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible", "metadataType": "", "metadata": null }, { "id": "8fda3ee5-90c5-4fbc-9b74-ac53f75e95a7", "name": "github.com/dustin/go-humanize", "version": "v1.0.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:dustin:go-humanize:v1.0.0:*:*:*:*:*:*:*", "cpe:2.3:a:dustin:go_humanize:v1.0.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/dustin/go-humanize@v1.0.0", "metadataType": "", "metadata": null }, { "id": "8b49fe37-0618-4dad-8379-692443ddc5e8", "name": "github.com/facebookincubator/nvdtools", "version": "v0.1.4", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:facebookincubator:nvdtools:v0.1.4:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/facebookincubator/nvdtools@v0.1.4", "metadataType": "", "metadata": null }, { "id": "04e5343e-1023-48a1-9423-79972c4e4214", "name": "github.com/go-test/deep", "version": "v1.0.7", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:go-test:deep:v1.0.7:*:*:*:*:*:*:*", "cpe:2.3:a:go_test:deep:v1.0.7:*:*:*:*:*:*:*", "cpe:2.3:a:go:deep:v1.0.7:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/go-test/deep@v1.0.7", "metadataType": "", "metadata": null }, { "id": "1ad992ea-1296-47e8-a468-4eb0c362a7e3", "name": "github.com/google/go-cmp", "version": "v0.4.1", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:google:go-cmp:v0.4.1:*:*:*:*:*:*:*", "cpe:2.3:a:google:go_cmp:v0.4.1:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/google/go-cmp@v0.4.1", "metadataType": "", "metadata": null }, { "id": "6d6cff34-9f29-450f-8cb8-ffe70775c1dd", "name": "github.com/google/uuid", "version": "v1.1.1", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:google:uuid:v1.1.1:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/google/uuid@v1.1.1", "metadataType": "", "metadata": null }, { "id": "b4202bbd-9fb5-4d6f-83bd-83a4aa6d1608", "name": "github.com/gookit/color", "version": "v1.4.2", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:gookit:color:v1.4.2:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/gookit/color@v1.4.2", "metadataType": "", "metadata": null }, { "id": "4ef230a8-22ef-4a45-92bb-fda1a17785bd", "name": "github.com/hashicorp/go-getter", "version": "v1.4.1", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:hashicorp:go_getter:v1.4.1:*:*:*:*:*:*:*", "cpe:2.3:a:hashicorp:go-getter:v1.4.1:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/hashicorp/go-getter@v1.4.1", "metadataType": "", "metadata": null }, { "id": "53de4a0f-f65c-4df4-90e7-9274471ae45e", "name": "github.com/hashicorp/go-multierror", "version": "v1.1.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:hashicorp:go-multierror:v1.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:hashicorp:go_multierror:v1.1.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/hashicorp/go-multierror@v1.1.0", "metadataType": "", "metadata": null }, { "id": "3eacac5b-f362-4d47-a509-3dfaa0f65bde", "name": "github.com/jinzhu/copier", "version": "v0.0.0-20190924061706-b57f9002281a", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:jinzhu:copier:v0.0.0-20190924061706-b57f9002281a:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/jinzhu/copier@v0.0.0-20190924061706-b57f9002281a", "metadataType": "", "metadata": null }, { "id": "ee1ac005-9c0d-45f4-a9b8-de9c805c8540", "name": "github.com/knqyf263/go-deb-version", "version": "v0.0.0-20190517075300-09fca494f03d", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:knqyf263:go-deb-version:v0.0.0-20190517075300-09fca494f03d:*:*:*:*:*:*:*", "cpe:2.3:a:knqyf263:go_deb_version:v0.0.0-20190517075300-09fca494f03d:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/knqyf263/go-deb-version@v0.0.0-20190517075300-09fca494f03d", "metadataType": "", "metadata": null }, { "id": "71970349-8c0d-41d9-b3f9-7dd5b50153ce", "name": "github.com/mitchellh/go-homedir", "version": "v1.1.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:mitchellh:go-homedir:v1.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:mitchellh:go_homedir:v1.1.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/mitchellh/go-homedir@v1.1.0", "metadataType": "", "metadata": null }, { "id": "fdf5f063-2178-454b-8e84-1412a65ee968", "name": "github.com/olekukonko/tablewriter", "version": "v0.0.4", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:olekukonko:tablewriter:v0.0.4:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/olekukonko/tablewriter@v0.0.4", "metadataType": "", "metadata": null }, { "id": "ed4372e1-d33e-4d18-9b5c-5c4b59e80662", "name": "github.com/pkg/profile", "version": "v1.6.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:pkg:profile:v1.6.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/pkg/profile@v1.6.0", "metadataType": "", "metadata": null }, { "id": "8870327f-8eef-4e2d-af6e-3b57e11d50a7", "name": "github.com/scylladb/go-set", "version": "v1.0.2", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:scylladb:go-set:v1.0.2:*:*:*:*:*:*:*", "cpe:2.3:a:scylladb:go_set:v1.0.2:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/scylladb/go-set@v1.0.2", "metadataType": "", "metadata": null }, { "id": "b1a92a31-3aaf-45ec-9876-37bd1de81f8d", "name": "github.com/sergi/go-diff", "version": "v1.1.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:sergi:go_diff:v1.1.0:*:*:*:*:*:*:*", "cpe:2.3:a:sergi:go-diff:v1.1.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/sergi/go-diff@v1.1.0", "metadataType": "", "metadata": null }, { "id": "432db78d-4f4e-4df3-a1d9-22a19a5cecd3", "name": "github.com/sirupsen/logrus", "version": "v1.6.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:sirupsen:logrus:v1.6.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/sirupsen/logrus@v1.6.0", "metadataType": "", "metadata": null }, { "id": "6cbdf0e8-95d6-4be6-b6db-ae1aed8495b3", "name": "github.com/spf13/afero", "version": "v1.3.2", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:spf13:afero:v1.3.2:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/spf13/afero@v1.3.2", "metadataType": "", "metadata": null }, { "id": "999d42fa-0df6-4fb3-b12a-4943ce613034", "name": "github.com/spf13/cobra", "version": "v1.0.1-0.20200909172742-8a63648dd905", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:spf13:cobra:v1.0.1-0.20200909172742-8a63648dd905:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/spf13/cobra@v1.0.1-0.20200909172742-8a63648dd905", "metadataType": "", "metadata": null }, { "id": "dccd69f3-390f-4480-a645-262e0d01452f", "name": "github.com/spf13/pflag", "version": "v1.0.5", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:spf13:pflag:v1.0.5:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/spf13/pflag@v1.0.5", "metadataType": "", "metadata": null }, { "id": "ca7a9196-0bc3-4044-a09f-03b90208d4ca", "name": "github.com/spf13/viper", "version": "v1.7.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:spf13:viper:v1.7.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/spf13/viper@v1.7.0", "metadataType": "", "metadata": null }, { "id": "203ee2fd-d494-4bd3-b563-edf0a217a625", "name": "github.com/stretchr/testify", "version": "v1.7.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:stretchr:testify:v1.7.0:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/stretchr/testify@v1.7.0", "metadataType": "", "metadata": null }, { "id": "a9b26b3c-b7fa-4896-a1d3-4d57b237e0e2", "name": "github.com/wagoodman/go-partybus", "version": "v0.0.0-20210627031916-db1f5573bbc5", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20210627031916-db1f5573bbc5:*:*:*:*:*:*:*", "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20210627031916-db1f5573bbc5:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20210627031916-db1f5573bbc5", "metadataType": "", "metadata": null }, { "id": "0fc965b2-dae1-48c2-b444-aa06c0444e3a", "name": "github.com/wagoodman/go-progress", "version": "v0.0.0-20200807221327-51d465df1451", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:wagoodman:go-progress:v0.0.0-20200807221327-51d465df1451:*:*:*:*:*:*:*", "cpe:2.3:a:wagoodman:go_progress:v0.0.0-20200807221327-51d465df1451:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/wagoodman/go-progress@v0.0.0-20200807221327-51d465df1451", "metadataType": "", "metadata": null }, { "id": "7d006c48-72c5-442d-b485-c69eae9c2bd8", "name": "github.com/wagoodman/jotframe", "version": "v0.0.0-20200730190914-3517092dd163", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:wagoodman:jotframe:v0.0.0-20200730190914-3517092dd163:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/wagoodman/jotframe@v0.0.0-20200730190914-3517092dd163", "metadataType": "", "metadata": null }, { "id": "179ada5b-cac6-49fe-a786-865874ab04ca", "name": "github.com/x-cray/logrus-prefixed-formatter", "version": "v0.5.2", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:x_cray:logrus_prefixed_formatter:v0.5.2:*:*:*:*:*:*:*", "cpe:2.3:a:x-cray:logrus_prefixed_formatter:v0.5.2:*:*:*:*:*:*:*", "cpe:2.3:a:x_cray:logrus-prefixed-formatter:v0.5.2:*:*:*:*:*:*:*", "cpe:2.3:a:x-cray:logrus-prefixed-formatter:v0.5.2:*:*:*:*:*:*:*", "cpe:2.3:a:x:logrus_prefixed_formatter:v0.5.2:*:*:*:*:*:*:*", "cpe:2.3:a:x:logrus-prefixed-formatter:v0.5.2:*:*:*:*:*:*:*" ], "purl": "pkg:golang/github.com/x-cray/logrus-prefixed-formatter@v0.5.2", "metadataType": "", "metadata": null }, { "id": "278908c5-bf45-4a37-9ef5-6ada37b2935a", "name": "golang.org/x/crypto", "version": "v0.0.0-20200622213623-75b288015ac9", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [ "cpe:2.3:a:golang:x/crypto:v0.0.0-20200622213623-75b288015ac9:*:*:*:*:*:*:*" ], "purl": "pkg:golang/golang.org/x/crypto@v0.0.0-20200622213623-75b288015ac9", "metadataType": "", "metadata": null }, { "id": "ccd900cd-78b1-4377-8265-2b1f2dff34a6", "name": "gopkg.in/yaml.v2", "version": "v2.3.0", "type": "go-module", "foundBy": "go-cataloger", "locations": [ { "path": "go.mod" } ], "licenses": [], "language": "go", "cpes": [], "purl": "pkg:golang/gopkg.in/yaml.v2@v2.3.0", "metadataType": "", "metadata": null }, { "id": "5582f267-e031-4687-99dc-3ee0d57cb724", "name": "joda-time", "version": "2.9.2", "type": "java-archive", "foundBy": "java-cataloger", "locations": [ { "path": "test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar" } ], "licenses": [], "language": "java", "cpes": [ "cpe:2.3:a:joda-time:joda-time:2.9.2:*:*:*:*:*:*:*", "cpe:2.3:a:joda_time:joda-time:2.9.2:*:*:*:*:*:*:*", "cpe:2.3:a:joda-time:joda_time:2.9.2:*:*:*:*:*:*:*", "cpe:2.3:a:joda_time:joda_time:2.9.2:*:*:*:*:*:*:*", "cpe:2.3:a:joda:joda-time:2.9.2:*:*:*:*:*:*:*", "cpe:2.3:a:joda:joda_time:2.9.2:*:*:*:*:*:*:*" ], "purl": "pkg:maven/joda-time/joda-time@2.9.2", "metadataType": "JavaMetadata", "metadata": { "virtualPath": "test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar:joda-time", "pomProperties": { "path": "META-INF/maven/joda-time/joda-time/pom.properties", "name": "", "groupId": "joda-time", "artifactId": "joda-time", "version": "2.9.2", "extraFields": {} }, "pomProject": { "path": "META-INF/maven/joda-time/joda-time/pom.xml", "groupId": "joda-time", "artifactId": "joda-time", "version": "2.9.2", "name": "Joda-Time", "description": "Date and time library to replace JDK date handling", "url": "http://www.joda.org/joda-time/" } } }, { "id": "f727dab3-3fe8-4082-b12d-0fed59452c89", "name": "libvncserver", "version": "0.9.9", "type": "apk", "foundBy": "apkdb-cataloger", "locations": [ { "path": "test/integration/testdata/image-alpine-match-coverage/lib/apk/db/installed" } ], "licenses": [ "GPL-2.0-or-later" ], "language": "", "cpes": [ "cpe:2.3:a:libvncserver:libvncserver:0.9.9:*:*:*:*:*:*:*" ], "purl": "pkg:alpine/libvncserver@0.9.9?arch=x86_64", "metadataType": "ApkMetadata", "metadata": { "package": "libvncserver", "originPackage": "libvncserver", "maintainer": "A. Wilcox ", "version": "0.9.9", "license": "GPL-2.0-or-later", "architecture": "x86_64", "url": "http://libvncserver.sourceforge.net/", "description": "Library to make writing a vnc server easy", "size": 166239, "installedSize": 389120, "pullDependencies": "so:libc.musl-x86_64.so.1 so:libgcrypt.so.20 so:libgnutls.so.30 so:libjpeg.so.8 so:libpng16.so.16 so:libz.so.1", "pullChecksum": "Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=", "gitCommitOfApkPort": "bf1ec813f662f128fc6b70f37ef1c0474bb24488", "files": [ { "path": "/usr", "digest": { "algorithm": "", "value": "" } }, { "path": "/usr/lib", "digest": { "algorithm": "", "value": "" } }, { "path": "/usr/lib/libvncclient.so.1", "ownerUid": "0", "ownerGid": "0", "permissions": "777", "digest": { "algorithm": "sha1", "value": "Q1quyp/JcSPFQhtQFjMUYdMwRvAWM=" } }, { "path": "/usr/lib/libvncserver.so.1.0.0", "ownerUid": "0", "ownerGid": "0", "permissions": "755", "digest": { "algorithm": "sha1", "value": "Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=" } }, { "path": "/usr/lib/libvncserver.so.1", "ownerUid": "0", "ownerGid": "0", "permissions": "777", "digest": { "algorithm": "sha1", "value": "Q184HrHsxEBqnsH4QNxeU5w8alhKI=" } }, { "path": "/usr/lib/libvncclient.so.1.0.0", "ownerUid": "0", "ownerGid": "0", "permissions": "755", "digest": { "algorithm": "sha1", "value": "Q1IEjCrEwVlQt2GjIsb3o39vcgqMg=" } } ] } }, { "id": "731abc1d-e0ed-4c6d-9554-1df437e4c540", "name": "rails", "version": "4.1.1", "type": "gem", "foundBy": "ruby-gemfile-cataloger", "locations": [ { "path": "test/integration/testdata/image-debian-match-coverage/ruby/Gemfile.lock" } ], "licenses": [], "language": "ruby", "cpes": [ "cpe:2.3:a:ruby_lang:rails:4.1.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby-lang:rails:4.1.1:*:*:*:*:*:*:*", "cpe:2.3:a:rails:rails:4.1.1:*:*:*:*:*:*:*", "cpe:2.3:a:ruby:rails:4.1.1:*:*:*:*:*:*:*", "cpe:2.3:a:*:rails:4.1.1:*:*:*:*:*:*:*" ], "purl": "pkg:gem/rails@4.1.1", "metadataType": "", "metadata": null } ], "artifactRelationships": [], "source": { "type": "directory", "target": "./" }, "distro": { "name": "", "version": "", "idLike": "" }, "descriptor": { "name": "syft", "version": "[not provided]" }, "schema": { "version": "1.1.0", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } ================================================ FILE: test/cli/testdata/sbom-ubuntu-20.04--pruned.json ================================================ { "artifacts": [ { "name": "gcc-10-base", "version": "10.2.0-5ubuntu1~20.04", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/gcc-10-base:amd64.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/gcc-10-base/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:gcc-10-base:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*", "cpe:2.3:a:*:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/gcc-10-base@10.2.0-5ubuntu1~20.04?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "gcc-10-base", "source": "gcc-10", "version": "10.2.0-5ubuntu1~20.04", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Core developers ", "installedSize": 260, "files": [ { "path": "/usr/share/doc/gcc-10-base/README.Debian.amd64.gz", "md5": "3c03902e06eef5dcfe3005376c23a120" }, { "path": "/usr/share/doc/gcc-10-base/TODO.Debian", "md5": "8afe308ec72834f3c24b209fbc4d149e" }, { "path": "/usr/share/doc/gcc-10-base/changelog.Debian.gz", "md5": "0e3cbc1152a18bddf7c24fe3913866c6" }, { "path": "/usr/share/doc/gcc-10-base/copyright", "md5": "a80ca2e181b9eecc3e4d373fd7ca59f2" } ] } }, { "name": "hostname", "version": "3.23", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/hostname.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/hostname/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:hostname:hostname:3.23:*:*:*:*:*:*:*", "cpe:2.3:a:*:hostname:3.23:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/hostname@3.23?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "hostname", "source": "", "version": "3.23", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Developers ", "installedSize": 54, "files": [ { "path": "/bin/hostname", "md5": "1ce73d718e3dccc1aaa7bce6ae2ef0a7" }, { "path": "/usr/share/doc/hostname/changelog.gz", "md5": "087a3eabd7427692c216a5d7a4341127" }, { "path": "/usr/share/doc/hostname/copyright", "md5": "460b6a1df2db2b5e80f05a44ec21c62f" }, { "path": "/usr/share/man/man1/hostname.1.gz", "md5": "62e6be6a928b4b9f2a985778fee171fd" } ] } }, { "name": "libacl1", "version": "2.2.53-6", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/libacl1:amd64.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/libacl1/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [ "GPL-2+", "LGPL-2+" ], "language": "", "cpes": [ "cpe:2.3:a:libacl1:libacl1:2.2.53-6:*:*:*:*:*:*:*", "cpe:2.3:a:*:libacl1:2.2.53-6:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/libacl1@2.2.53-6?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "libacl1", "source": "acl", "version": "2.2.53-6", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Developers ", "installedSize": 70, "files": [ { "path": "/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2253", "md5": "e77bf61a72656a594ef49768a7d6097b" }, { "path": "/usr/share/doc/libacl1/changelog.Debian.gz", "md5": "65de3b787d67d4755ad3ae0584aee9f2" }, { "path": "/usr/share/doc/libacl1/copyright", "md5": "40822d07cf4c0fb9ab13c2bebf51d981" } ] } }, { "name": "libattr1", "version": "1:2.4.48-5", "type": "deb", "foundBy": "dpkgdb-cataloger", "locations": [ { "path": "/var/lib/dpkg/status", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/var/lib/dpkg/info/libattr1:amd64.md5sums", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" }, { "path": "/usr/share/doc/libattr1/copyright", "layerID": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009" } ], "licenses": [ "GPL-2+", "LGPL-2+" ], "language": "", "cpes": [ "cpe:2.3:a:libattr1:libattr1:1:2.4.48-5:*:*:*:*:*:*:*", "cpe:2.3:a:*:libattr1:1:2.4.48-5:*:*:*:*:*:*:*" ], "purl": "pkg:deb/ubuntu/libattr1@1:2.4.48-5?arch=amd64", "metadataType": "DpkgMetadata", "metadata": { "package": "libattr1", "source": "attr", "version": "1:2.4.48-5", "sourceVersion": "", "architecture": "amd64", "maintainer": "Ubuntu Developers ", "installedSize": 57, "files": [ { "path": "/usr/lib/x86_64-linux-gnu/libattr.so.1.1.2448", "md5": "708453da8ebde1aaca2ca69c04d4c0a8" }, { "path": "/usr/share/doc/libattr1/changelog.Debian.gz", "md5": "6465a4cda28287d4ea9979b530648ee3" }, { "path": "/usr/share/doc/libattr1/copyright", "md5": "1e0c5c8b55170890f960aad90336aaed" } ] } } ], "source": { "type": "image", "target": { "userInput": "ubuntu:20.04", "imageID": "sha256:f63181f19b2fe819156dcb068b3b5bc036820bec7014c5f77277cfa341d4cb5e", "manifestDigest": "sha256:5146935f9248826d44dfc2489abfd5f4bdfbc319a738c04dfe1ef071f228a1ac", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [ "ubuntu:20.04" ], "imageSize": 72898411, "scope": "Squashed", "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009", "size": 72897593 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:dbf2c0f42a39b60301f6d3936f7f8adb59bb97d31ec11cc4a049ce81155fef89", "size": 811 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:02473afd360bd5391fa51b6e7849ce88732ae29f50f3630c3551f528eba66d1e", "size": 7 } ] } }, "distro": { "name": "ubuntu", "version": "20.04", "idLike": "debian" }, "descriptor": { "name": "syft", "version": "0.12.7" }, "schema": { "version": "1.0.1", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.1.json" } } ================================================ FILE: test/cli/testdata/test-ignore-reason/config-with-ignore.yaml ================================================ check-for-app-update: false ignore: - vulnerability: CVE-2021-42385 reason: test reason for vulnerability being ignored ================================================ FILE: test/cli/testdata/test-ignore-reason/sbom.json ================================================ { "artifacts": [ { "id": "20a169629a73fdbd", "name": "busybox", "version": "1.31.1", "type": "binary", "foundBy": "binary-cataloger", "locations": [ { "path": "/bin/[", "layerID": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f", "annotations": { "evidence": "primary" } } ], "licenses": [], "language": "", "cpes": [ "cpe:2.3:a:busybox:busybox:1.31.1:*:*:*:*:*:*:*", "cpe:2.3:a:busybox:busybox:1.31.1:*:*:*:*:*:*:*" ], "purl": "", "metadataType": "BinaryMetadata", "metadata": { "matches": [ { "classifier": "busybox-binary", "location": { "path": "/bin/[", "layerID": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f", "annotations": { "evidence": "primary" } } } ] } } ], "artifactRelationships": [ { "parent": "1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853", "child": "20a169629a73fdbd", "type": "contains" }, { "parent": "20a169629a73fdbd", "child": "1c3ded193f8808da", "type": "evident-by" } ], "files": [ { "id": "1c3ded193f8808da", "location": { "path": "/bin/[", "layerID": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f" } } ], "source": { "id": "1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853", "name": "busybox", "version": "1.31", "type": "image", "metadata": { "userInput": "busybox:1.31", "imageID": "sha256:19d689bc58fd64da6a46d46512ea965a12b6bfb5b030400e21bc0a04c4ff155e", "manifestDigest": "sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853", "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "tags": [], "imageSize": 1384134, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "digest": "sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f", "size": 1384134 } ], "manifest": "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ5NCwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6MTlkNjg5YmM1OGZkNjRkYTZhNDZkNDY1MTJlYTk2NWExMmI2YmZiNWIwMzA0MDBlMjFiYzBhMDRjNGZmMTU1ZSIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDgxNTQwMCwKICAgICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6ZmQ0NDAxNmUzZDNlZGI4ZDFkOGIxYTMzNTdmNzcwOGE4Mzg4ZTQ2MjI1MDBkMzZmZGUzYThjZGZiMjNmYmFjOCIKICAgICAgfQogICBdCn0=", "config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbInNoIl0sIkFyZ3NFc2NhcGVkIjp0cnVlLCJJbWFnZSI6InNoYTI1NjpmNDFmZmIxYWI1MWNhMDg3YzdkN2E3MWM5NDUzMGNmOWM4MjFkZDM3Zjc2ZGU0NjY2NTZmNjM1NDE0NGQzYmQyIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lciI6IjBlY2U0NzFlM2MyMGFmYTQyYWM2ZTRhNzBlYTc0MDFmM2ViNGNiNzUzZmQ0MzYyZDA3ZDNmNWI1ZjFlODhhZDEiLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiMGVjZTQ3MWUzYzIwIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApICIsIkNNRCBbXCJzaFwiXSJdLCJBcmdzRXNjYXBlZCI6dHJ1ZSwiSW1hZ2UiOiJzaGEyNTY6ZjQxZmZiMWFiNTFjYTA4N2M3ZDdhNzFjOTQ1MzBjZjljODIxZGQzN2Y3NmRlNDY2NjU2ZjYzNTQxNDRkM2JkMiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOnt9fSwiY3JlYXRlZCI6IjIwMjAtMDYtMDJUMjE6Mzk6NDUuMzc0Mjg5MDQxWiIsImRvY2tlcl92ZXJzaW9uIjoiMTguMDkuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIwLTA2LTAyVDIxOjM5OjQ0LjUzMDIwOTg5MVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6MDdkOTQ2NmVkMWExMDkxNmY0ODIzZGI2OWY0YzU4NDg0Y2U3MDIyMWMzYzk0YTBmMmE4MDRmNTM3MWE2Mjc2NSBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjAtMDYtMDJUMjE6Mzk6NDUuMzc0Mjg5MDQxWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcInNoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjdjZTM3ODQ0Y2E3NTYwMGRiY2JlMDg1ODU4ODQ1YzViOTJiNjEwOWRiM2M4YzFhZTZlYjg4N2FhYjkxYWQwNGYiXX19", "repoDigests": [ "index.docker.io/library/busybox@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209" ], "architecture": "arm64", "os": "linux" } }, "distro": { "prettyName": "BusyBox v1.31.1", "name": "busybox", "id": "busybox", "idLike": [ "busybox" ], "version": "1.31.1", "versionID": "1.31.1" }, "descriptor": { "name": "syft", "version": "0.94.0", "configuration": { "catalogers": null, "package": { "cataloger": { "enabled": true, "scope": "Squashed" }, "search-unindexed-archives": false, "search-indexed-archives": true }, "golang": { "search-local-mod-cache-licenses": false, "local-mod-cache-dir": "", "search-remote-licenses": false, "proxy": "", "no-proxy": "" }, "linux-kernel": { "catalog-modules": true }, "python": { "guess-unpinned-requirements": false }, "file-metadata": { "cataloger": { "enabled": false, "scope": "Squashed" }, "digests": [ "sha256" ] }, "file-classification": { "cataloger": { "enabled": false, "scope": "Squashed" } }, "file-contents": { "cataloger": { "enabled": false, "scope": "Squashed" }, "skip-files-above-size": 1048576, "globs": null }, "secrets": { "cataloger": { "enabled": false, "scope": "AllLayers" }, "additional-patterns": null, "exclude-pattern-names": null, "reveal-values": false, "skip-files-above-size": 1048576 }, "registry": { "insecure-skip-tls-verify": false, "insecure-use-http": false, "auth": null, "ca-cert": "" }, "exclude": [], "platform": "", "name": "", "source": { "name": "", "version": "", "file": { "digests": [ "sha256" ] } }, "parallelism": 1, "default-image-pull-source": "", "base-path": "", "exclude-binary-overlap-by-ownership": true } }, "schema": { "version": "11.0.1", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-11.0.1.json" } } ================================================ FILE: test/cli/testdata/test-ignore-reason/template-with-ignore-reasons ================================================ The following vulnerabilities are considered irrelevant: {{- range .IgnoredMatches}} {{.Vulnerability.ID}} ({{ range $air := .AppliedIgnoreRules }}{{ $air.Reason }}{{ end }}) {{- end}} ================================================ FILE: test/cli/trait_assertions_test.go ================================================ package cli import ( "encoding/json" "strings" "testing" "github.com/acarl005/stripansi" ) type traitAssertion func(tb testing.TB, stdout, stderr string, rc int) func assertNoStderr(tb testing.TB, _, stderr string, _ int) { tb.Helper() if len(stderr) > 0 { tb.Errorf("expected stderr to be empty, but wasn't") } } func assertInOutput(data string) traitAssertion { return func(tb testing.TB, stdout, stderr string, _ int) { tb.Helper() if !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) { tb.Errorf("data=%q was NOT found in any output, but should have been there", data) } } } func assertFailingReturnCode(tb testing.TB, _, _ string, rc int) { tb.Helper() if rc == 0 { tb.Errorf("expected a failure but got rc=%d", rc) } } func assertSucceedingReturnCode(tb testing.TB, _, _ string, rc int) { tb.Helper() if rc != 0 { tb.Errorf("expected to succeed but got rc=%d", rc) } } func assertRowInStdOut(row []string) traitAssertion { return func(tb testing.TB, stdout, stderr string, _ int) { tb.Helper() for _, line := range strings.Split(stdout, "\n") { lineMatched := false for _, column := range row { if !strings.Contains(line, column) { // it wasn't this line lineMatched = false break } lineMatched = true } if lineMatched { return } } // none of the lines matched tb.Errorf("expected stdout to contain %s, but it did not", strings.Join(row, " ")) } } func assertNotInOutput(notWanted string) traitAssertion { return func(tb testing.TB, stdout, stderr string, _ int) { if strings.Contains(stdout, notWanted) { tb.Errorf("got unwanted %s in stdout %s", notWanted, stdout) } } } func assertJsonReport(tb testing.TB, stdout, _ string, _ int) { tb.Helper() var data interface{} if err := json.Unmarshal([]byte(stdout), &data); err != nil { tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err) } } func assertDbProvidersTableReport(tb testing.TB, stdout, _ string, _ int) { tb.Helper() if !strings.Contains(stdout, "NAME") || !strings.Contains(stdout, "DATE CAPTURED") { tb.Errorf("expected to find a table report, but did not") } } ================================================ FILE: test/cli/utils_test.go ================================================ package cli import ( "bytes" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/anchore/stereoscope/pkg/imagetest" ) func getFixtureImage(tb testing.TB, fixtureImageName string) string { tb.Helper() imagetest.GetFixtureImage(tb, "docker-archive", fixtureImageName) return imagetest.GetFixtureImageTarPath(tb, fixtureImageName) } func getGrypeCommand(tb testing.TB, args ...string) *exec.Cmd { tb.Helper() argsWithConfig := args if !grypeCommandHasConfigArg(argsWithConfig...) { argsWithConfig = append( []string{"-c", "../grype-test-config.yaml"}, args..., ) } return exec.Command( getGrypeSnapshotLocation(tb, runtime.GOOS), argsWithConfig..., ) } func grypeCommandHasConfigArg(args ...string) bool { for _, arg := range args { if arg == "-c" || arg == "--config" { return true } } return false } func getGrypeSnapshotLocation(t testing.TB, goOS string) string { // GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary const envKey = "GRYPE_BINARY_LOCATION" if os.Getenv(envKey) != "" { return os.Getenv(envKey) } loc := getGrypeBinaryLocationByOS(t, goOS) buildBinary(t, loc) _ = os.Setenv(envKey, loc) return loc } func getGrypeBinaryLocationByOS(t testing.TB, goOS string) string { // note: for amd64 we need to update the snapshot location with the v1 suffix // see : https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-builds archPath := runtime.GOARCH if runtime.GOARCH == "amd64" { archPath = fmt.Sprintf("%s_v1", archPath) } executable := "grype" // note: there is a subtle - vs _ difference between these versions switch goOS { case "windows": executable += ".exe" fallthrough case "darwin", "linux": return filepath.Join(repoRoot(t), "snapshot", fmt.Sprintf("%s-build_%s_%s", goOS, goOS, archPath), executable) default: t.Fatalf("unsupported OS: %s", runtime.GOOS) } return "" } func buildBinary(t testing.TB, loc string) { t.Chdir(repoRoot(t)) t.Log("Building grype...") c := exec.Command("go", "build", "-o", loc, "./cmd/grype") c.Stdout = os.Stdout c.Stderr = os.Stderr c.Stdin = os.Stdin require.NoError(t, c.Run()) } func getDockerRunCommand(tb testing.TB, args ...string) *exec.Cmd { tb.Helper() return exec.Command( "docker", append( []string{"run"}, args..., )..., ) } func runGrype(tb testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { tb.Helper() cmd := getGrypeCommand(tb, args...) if env == nil { env = make(map[string]string) } // we should not have tests reaching out for app update checks env["GRYPE_CHECK_FOR_APP_UPDATE"] = "false" stdout, stderr := runCommand(cmd, env) return cmd, stdout, stderr } func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) { if env != nil { cmd.Env = append(os.Environ(), envMapToSlice(env)...) } var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // ignore errors since this may be what the test expects cmd.Run() return stdout.String(), stderr.String() } func envMapToSlice(env map[string]string) (envList []string) { for key, val := range env { if key == "" { continue } envList = append(envList, fmt.Sprintf("%s=%s", key, val)) } return } func repoRoot(tb testing.TB) string { tb.Helper() root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { tb.Fatalf("unable to find repo root dir: %+v", err) } absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) if err != nil { tb.Fatal("unable to get abs path to repo root:", err) } return absRepoRoot } func attachFileToCommandStdin(tb testing.TB, file io.Reader, command *exec.Cmd) { tb.Helper() b, err := io.ReadAll(file) require.NoError(tb, err) command.Stdin = bytes.NewReader(b) } func assertCommandExecutionSuccess(t testing.TB, cmd *exec.Cmd) { _, err := cmd.CombinedOutput() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { t.Fatal(exitErr) } t.Fatalf("unable to run command %q: %v", cmd, err) } } func testWithTimeout(t *testing.T, name string, timeout time.Duration, test func(*testing.T)) { done := make(chan bool) go func() { t.Run(name, test) done <- true }() select { case <-time.After(timeout): t.Fatal("test timed out") case <-done: } } ================================================ FILE: test/cli/version_cmd_test.go ================================================ package cli import ( "testing" ) func TestVersionCmdPrintsToStdout(t *testing.T) { tests := []struct { name string env map[string]string assertions []traitAssertion }{ { name: "version command prints to stdout", assertions: []traitAssertion{ assertInOutput("Version:"), assertNoStderr, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { pkgCmd, pkgsStdout, pkgsStderr := runGrype(t, test.env, "version") for _, traitFn := range test.assertions { traitFn(t, pkgsStdout, pkgsStderr, pkgCmd.ProcessState.ExitCode()) } }) } } ================================================ FILE: test/grype-test-config.yaml ================================================ check-for-app-update: false ================================================ FILE: test/ignore-att-signature.yaml ================================================ check-for-app-update: false ================================================ FILE: test/install/.dockerignore ================================================ ** ================================================ FILE: test/install/.gitignore ================================================ cache/ ================================================ FILE: test/install/0_checksums_test.sh ================================================ . test_harness.sh # search for an asset in a release checksums file test_search_for_asset_release() { fixture=./testdata/grype_0.32.0_checksums.txt # search_for_asset [checksums-file-path] [name] [os] [arch] [format] # positive case actual=$(search_for_asset "${fixture}" "grype" "linux" "amd64" "tar.gz") assertEquals "grype_0.32.0_linux_amd64.tar.gz" "${actual}" "unable to find release asset" # negative cases actual=$(search_for_asset "${fixture}" "grype" "Linux" "amd64" "tar.gz") assertEquals "" "${actual}" "found a release asset but did not expect to (os)" actual=$(search_for_asset "${fixture}" "grype" "darwin" "amd64" "rpm") assertEquals "" "${actual}" "found a release asset but did not expect to (format)" } run_test_case test_search_for_asset_release # search for an asset in a snapshot checksums file test_search_for_asset_snapshot() { fixture=./testdata/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt # search_for_asset [checksums-file-path] [name] [os] [arch] [format] # positive case actual=$(search_for_asset "${fixture}" "grype" "linux" "amd64" "rpm") assertEquals "grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.rpm" "${actual}" "unable to find snapshot asset" # negative case actual=$(search_for_asset "${fixture}" "grype" "linux" "amd64" "zip") assertEquals "" "${actual}" "found a snapshot asset but did not expect to (format)" } run_test_case test_search_for_asset_snapshot # verify 256 digest of a file test_hash_sha256() { target=./testdata/assets/valid/grype_0.78.0_linux_arm64.tar.gz # hash_sha256 [target] # positive case actual=$(hash_sha256 "${target}") assertEquals "8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d" "${actual}" "mismatched checksum" } run_test_case test_hash_sha256 # verify 256 digest of a file relative to the checksums file test_hash_sha256_verify() { # hash_sha256_verify [target] [checksums] # positive case checksums=./testdata/assets/valid/checksums.txt target=./testdata/assets/valid/grype_0.78.0_linux_arm64.tar.gz hash_sha256_verify "${target}" "${checksums}" assertEquals "0" "$?" "mismatched checksum" # negative case # we are expecting error messages, which is confusing to look at in passing tests... disable logging for now log_set_priority -1 checksums=./testdata/assets/invalid/checksums.txt target=./testdata/assets/invalid/grype_0.78.0_linux_arm64.tar.gz hash_sha256_verify "${target}" "${checksums}" assertEquals "1" "$?" "verification did not catch mismatched checksum" # restore logging... log_set_priority 0 } run_test_case test_hash_sha256_verify ================================================ FILE: test/install/1_download_snapshot_asset_test.sh ================================================ . test_harness.sh DOWNLOAD_SNAPSHOT_POSITIVE_CASES=0 # helper for asserting test_positive_snapshot_download_asset positive cases test_positive_snapshot_download_asset() { os="$1" arch="$2" format="$3" # for troubleshooting # log_set_priority 10 name=${PROJECT_NAME} github_download=$(snapshot_download_url) version=$(snapshot_version) tmpdir=$(mktemp -d) actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}" ) assertFileExists "${actual_filepath}" "download_asset os=${os} arch=${arch} format=${format}" assertFilesEqual \ "$(snapshot_dir)/${name}_${version}_${os}_${arch}.${format}" \ "${actual_filepath}" \ "unable to download os=${os} arch=${arch} format=${format}" ((DOWNLOAD_SNAPSHOT_POSITIVE_CASES++)) rm -rf -- "$tmpdir" } test_download_snapshot_asset_exercised_all_assets() { expected=$(snapshot_assets_count) assertEquals "${expected}" "${DOWNLOAD_SNAPSHOT_POSITIVE_CASES}" "did not download all possible assets (missing an os/arch/format variant?)" } # helper for asserting download_asset negative cases test_negative_snapshot_download_asset() { os="$1" arch="$2" format="$3" # for troubleshooting # log_set_priority 10 name=${PROJECT_NAME} github_download=$(snapshot_download_url) version=$(snapshot_version) tmpdir=$(mktemp -d) actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}") assertEquals "" "${actual_filepath}" "unable to download os=${os} arch=${arch} format=${format}" rm -rf -- "$tmpdir" } worker_pid=$(setup_snapshot_server) trap 'teardown_snapshot_server ${worker_pid}' EXIT # exercise all possible assets run_test_case test_positive_snapshot_download_asset "linux" "amd64" "tar.gz" run_test_case test_positive_snapshot_download_asset "linux" "amd64" "rpm" run_test_case test_positive_snapshot_download_asset "linux" "amd64" "deb" run_test_case test_positive_snapshot_download_asset "linux" "arm64" "tar.gz" run_test_case test_positive_snapshot_download_asset "linux" "arm64" "rpm" run_test_case test_positive_snapshot_download_asset "linux" "arm64" "deb" run_test_case test_positive_snapshot_download_asset "linux" "s390x" "tar.gz" run_test_case test_positive_snapshot_download_asset "linux" "s390x" "rpm" run_test_case test_positive_snapshot_download_asset "linux" "s390x" "deb" run_test_case test_positive_snapshot_download_asset "linux" "ppc64le" "tar.gz" run_test_case test_positive_snapshot_download_asset "linux" "ppc64le" "rpm" run_test_case test_positive_snapshot_download_asset "linux" "ppc64le" "deb" run_test_case test_positive_snapshot_download_asset "darwin" "amd64" "tar.gz" run_test_case test_positive_snapshot_download_asset "darwin" "arm64" "tar.gz" run_test_case test_positive_snapshot_download_asset "windows" "amd64" "zip" # note: the mac signing process produces a dmg which is not part of the snapshot process (thus is not exercised here) # let's make certain we covered all assets that were expected run_test_case test_download_snapshot_asset_exercised_all_assets # make certain we handle missing assets alright run_test_case test_negative_snapshot_download_asset "bogus" "amd64" "zip" trap - EXIT teardown_snapshot_server "${worker_pid}" ================================================ FILE: test/install/2_download_release_asset_test.sh ================================================ . test_harness.sh test_download_release_asset() { release="$1" os="$2" arch="$3" format="$4" expected_mime_type="$5" # for troubleshooting # log_set_priority 10 name=${PROJECT_NAME} version=$(tag_to_version ${release}) github_download="https://github.com/${OWNER}/${REPO}/releases/download/${release}" tmpdir=$(mktemp -d) actual_filepath=$(download_asset "${github_download}" "${tmpdir}" "${name}" "${os}" "${arch}" "${version}" "${format}" ) assertFileExists "${actual_filepath}" "download_asset os=${os} arch=${arch} format=${format}" actual_mime_type=$(file -b --mime-type ${actual_filepath}) assertEquals "${expected_mime_type}" "${actual_mime_type}" "unexpected mimetype for os=${os} arch=${arch} format=${format}" rm -rf -- "$tmpdir" } # always test against the latest release release=$(get_release_tag "${OWNER}" "${REPO}" "latest" ) # exercise all possible assets against a real github release (based on asset listing from https://github.com/anchore/grype/releases/tag/v0.32.0) # verify all downloads against the checksums file + checksums file signature VERIFY_SIGN=true run_test_case test_download_release_asset "${release}" "darwin" "amd64" "tar.gz" "application/gzip" run_test_case test_download_release_asset "${release}" "darwin" "arm64" "tar.gz" "application/gzip" run_test_case test_download_release_asset "${release}" "linux" "amd64" "tar.gz" "application/gzip" run_test_case test_download_release_asset "${release}" "linux" "amd64" "rpm" "application/x-rpm" run_test_case test_download_release_asset "${release}" "linux" "amd64" "deb" "application/vnd.debian.binary-package" run_test_case test_download_release_asset "${release}" "linux" "arm64" "tar.gz" "application/gzip" run_test_case test_download_release_asset "${release}" "linux" "arm64" "rpm" "application/x-rpm" run_test_case test_download_release_asset "${release}" "linux" "arm64" "deb" "application/vnd.debian.binary-package" ================================================ FILE: test/install/3_install_asset_test.sh ================================================ . test_harness.sh INSTALL_ARCHIVE_POSITIVE_CASES=0 # helper for asserting install_asset positive cases test_positive_snapshot_install_asset() { os="$1" arch="$2" format="$3" # for troubleshooting # log_set_priority 10 name=${PROJECT_NAME} binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}") github_download=$(snapshot_download_url) version=$(snapshot_version) download_dir=$(mktemp -d) install_dir=$(mktemp -d) download_and_install_asset "${github_download}" "${download_dir}" "${install_dir}" "${name}" "${os}" "${arch}" "${version}" "${format}" "${binary}" assertEquals "0" "$?" "download/install did not succeed" expected_path="${install_dir}/${binary}" assertFileExists "${expected_path}" "install_asset os=${os} arch=${arch} format=${format}" # directory structure for arch has been updated as of go 1.18 # https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-buildsjk if [ $arch == "amd64" ]; then arch="amd64_v1" fi local_suffix="" if [ "${arch}" == "arm64" ]; then local_suffix="_v8.0" fi if [ "${arch}" == "ppc64le" ]; then local_suffix="_power8" fi assertFilesEqual \ "$(snapshot_dir)/${os}-build_${os}_${arch}${local_suffix}/${binary}" \ "${expected_path}" \ "unable to verify installation of os=${os} arch=${arch} format=${format}" ((INSTALL_ARCHIVE_POSITIVE_CASES++)) rm -rf -- "$download_dir" rm -rf -- "$install_dir" } # helper for asserting install_asset negative cases test_negative_snapshot_install_asset() { os="$1" arch="$2" format="$3" # for troubleshooting # log_set_priority 10 name=${PROJECT_NAME} binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}") github_download=$(snapshot_download_url) version=$(snapshot_version) download_dir=$(mktemp -d) install_dir=$(mktemp -d) download_and_install_asset "${github_download}" "${download_dir}" "${install_dir}" "${name}" "${os}" "${arch}" "${version}" "${format}" "${binary}" assertNotEquals "0" "$?" "download/install should have failed but did not" rm -rf -- "$download_dir" rm -rf -- "$install_dir" } test_install_asset_exercised_all_archive_assets() { expected=$(snapshot_assets_archive_count) assertEquals "${expected}" "${INSTALL_ARCHIVE_POSITIVE_CASES}" "did not download all possible archive assets (missing an os/arch/format variant?)" } worker_pid=$(setup_snapshot_server) trap 'teardown_snapshot_server ${worker_pid}' EXIT # exercise all possible archive assets (not rpm/deb/dmg) against a snapshot build run_test_case test_positive_snapshot_install_asset "linux" "amd64" "tar.gz" run_test_case test_positive_snapshot_install_asset "linux" "arm64" "tar.gz" run_test_case test_positive_snapshot_install_asset "linux" "s390x" "tar.gz" run_test_case test_positive_snapshot_install_asset "linux" "ppc64le" "tar.gz" run_test_case test_positive_snapshot_install_asset "darwin" "amd64" "tar.gz" run_test_case test_positive_snapshot_install_asset "darwin" "arm64" "tar.gz" run_test_case test_positive_snapshot_install_asset "windows" "amd64" "zip" # let's make certain we covered all assets that were expected run_test_case test_install_asset_exercised_all_archive_assets # make certain we handle missing assets alright run_test_case test_negative_snapshot_install_asset "bogus" "amd64" "zip" trap - EXIT teardown_snapshot_server "${worker_pid}" ================================================ FILE: test/install/4_prep_signature_verification_test.sh ================================================ . test_harness.sh test_compare_semver() { # compare_semver [version1] [version2] # positive cases (version1 >= version2) compare_semver "0.32.0" "0.32.0" assertEquals "0" "$?" "+ versions should equal" compare_semver "0.32.1" "0.32.0" assertEquals "0" "$?" "+ patch version should be greater" compare_semver "0.33.0" "0.32.0" assertEquals "0" "$?" "+ minor version should be greater" compare_semver "0.333.0" "0.32.0" assertEquals "0" "$?" "+ minor version should be greater (different length)" compare_semver "00.33.00" "0.032.0" assertEquals "0" "$?" "+ minor version should be greater (different length reversed)" compare_semver "1.0.0" "0.9.9" assertEquals "0" "$?" "+ major version should be greater" compare_semver "v1.0.0" "1.0.0" assertEquals "0" "$?" "+ can remove leading 'v' from version" # negative cases (version1 < version2) compare_semver "0.32.0" "0.32.1" assertEquals "1" "$?" "- patch version should be less" compare_semver "0.32.7" "0.33.0" assertEquals "1" "$?" "- minor version should be less" compare_semver "00.00032.070" "0.33.0" assertEquals "1" "$?" "- minor version should be less (different length)" compare_semver "0.32.7" "00.0033.000" assertEquals "1" "$?" "- minor version should be less (different length reversed)" compare_semver "1.9.9" "2.0.1" assertEquals "1" "$?" "- major version should be less" compare_semver "1.0.0" "v2.0.0" assertEquals "1" "$?" "- can remove leading 'v' from version" } run_test_case test_compare_semver # ensure that various signature verification pre-requisites are correctly checked for test_prep_signature_verification() { # prep_sign_verification [version] # we are expecting error messages, which is confusing to look at in passing tests... disable logging for now log_set_priority -1 # backup original values... OG_COSIGN_BINARY=${COSIGN_BINARY} # check the verification path... VERIFY_SIGN=true # release does not support signature verification prep_signature_verification "0.71.0" assertEquals "1" "$?" "release does not support signature verification" # check that the COSIGN binary exists COSIGN_BINARY=fake-cosign-that-doesnt-exist prep_signature_verification "0.80.0" assertEquals "1" "$?" "cosign binary verification failed" # restore original values... COSIGN_BINARY=${OG_COSIGN_BINARY} # ignore any failing conditions since we are not verifying the signature VERIFY_SIGN=false prep_signature_verification "0.71.0" assertEquals "0" "$?" "release support verification should not have been triggered" COSIGN_BINARY=fake-cosign-that-doesnt-exist prep_signature_verification "0.80.0" assertEquals "0" "$?" "cosign binary verification should not have been triggered" # restore original values... COSIGN_BINARY=${OG_COSIGN_BINARY} # restore logging... log_set_priority 0 } run_test_case test_prep_signature_verification ================================================ FILE: test/install/Makefile ================================================ NAME=grype # for local testing (not testing within containers) use the binny-managed version of cosign. # this also means that the user does not need to install cosign on their system to run tests. COSIGN_BINARY=../../.tool/cosign IMAGE_NAME=$(NAME)-install.sh-env UBUNTU_IMAGE=$(IMAGE_NAME):ubuntu-20.04 ALPINE_IMAGE=$(IMAGE_NAME):alpine-3.6 BUSYBOX_IMAGE=$(IMAGE_NAME):busybox-1.36 ENVS=./environments DOCKER_RUN=docker run --rm -t -w /project/test/install -v $(shell pwd)/../../:/project UNIT=make unit-run # acceptance testing is running the current install.sh against the latest release. Note: this could be a problem down # the line if there are breaking changes made that don't align with the latest release (but will be OK with the next # release). This tests both installing with signature verification and without. ACCEPTANCE_CMD=sh -c '../../install.sh -v -b /usr/local/bin && grype version && rm /usr/local/bin/grype && ../../install.sh -b /usr/local/bin && grype version' # we also want to test against a previous release to ensure that install.sh defers execution to a former install.sh # this version should be at least as recent as when grype was publishing for darwin arm64 as that is what the github runner uses for osx validation PREVIOUS_RELEASE=v0.60.0 ACCEPTANCE_PREVIOUS_RELEASE_CMD=sh -c "../../install.sh -b /usr/local/bin $(PREVIOUS_RELEASE) && grype version" # CI cache busting values; change these if you want CI to not use previous stored cache INSTALL_TEST_CACHE_BUSTER=894d8ca define title @printf '\n≡≡≡[ $(1) ]≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡\n' endef .PHONY: test test: unit acceptance .PHONY: ci-test-mac ci-test-mac: unit-run acceptance-local # note: do not add acceptance-local to this list .PHONY: acceptance acceptance: acceptance-ubuntu-20.04 acceptance-alpine-3.6 acceptance-busybox-1.36 .PHONY: unit unit: unit-ubuntu-20.04 .PHONY: unit-local unit-local: $(call title,unit tests) @for f in $(shell ls *_test.sh); do echo "Running unit test suite '$${f}'"; bash -c "COSIGN_BINARY=$(COSIGN_BINARY) ./$${f}" || exit 1; done .PHONY: unit-run unit-run: $(call title,unit tests) @for f in $(shell ls *_test.sh); do echo "Running unit test suite '$${f}'"; bash $${f} || exit 1; done .PHONY: acceptance-local acceptance-local: acceptance-current-release-local acceptance-previous-release-local .PHONY: acceptance-current-release-local acceptance-current-release-local: $(ACCEPTANCE_CMD) .PHONY: acceptance-previous-release-local acceptance-previous-release-local: $(ACCEPTANCE_PREVIOUS_RELEASE_CMD) grype version | grep $(shell echo $(PREVIOUS_RELEASE)| tr -d "v") .PHONY: save save: ubuntu-20.04 alpine-3.6 busybox-1.36 @mkdir cache || true docker image save -o cache/ubuntu-env.tar $(UBUNTU_IMAGE) docker image save -o cache/alpine-env.tar $(ALPINE_IMAGE) docker image save -o cache/busybox-env.tar $(BUSYBOX_IMAGE) .PHONY: load load: docker image load -i cache/ubuntu-env.tar docker image load -i cache/alpine-env.tar docker image load -i cache/busybox-env.tar ## UBUNTU ####################################################### .PHONY: acceptance-ubuntu-20.04 acceptance-ubuntu-20.04: ubuntu-20.04 $(call title,ubuntu:20.04 - acceptance) $(DOCKER_RUN) $(UBUNTU_IMAGE) \ $(ACCEPTANCE_CMD) .PHONY: unit-ubuntu-20.04 unit-ubuntu-20.04: ubuntu-20.04 $(call title,ubuntu:20.04 - unit) $(DOCKER_RUN) $(UBUNTU_IMAGE) \ $(UNIT) .PHONY: ubuntu-20.04 ubuntu-20.04: $(call title,ubuntu:20.04 - build environment) docker build -t $(UBUNTU_IMAGE) -f $(ENVS)/Dockerfile-ubuntu-20.04 . ## ALPINE ####################################################### # note: unit tests cannot be run with sh (alpine dosn't have bash by default) .PHONY: acceptance-alpine-3.6 acceptance-alpine-3.6: alpine-3.6 $(call title,alpine:3.6 - acceptance) $(DOCKER_RUN) $(ALPINE_IMAGE) \ $(ACCEPTANCE_CMD) .PHONY: alpine-3.6 alpine-3.6: $(call title,alpine:3.6 - build environment) docker build -t $(ALPINE_IMAGE) -f $(ENVS)/Dockerfile-alpine-3.6 . ## BUSYBOX ####################################################### # note: unit tests cannot be run with sh (busybox dosn't have bash by default) # note: busybox by default will not have cacerts, so you will get TLS warnings (we want to test under these conditions) .PHONY: acceptance-busybox-1.36 acceptance-busybox-1.36: busybox-1.36 $(call title,busybox-1.36 - acceptance) $(DOCKER_RUN) $(BUSYBOX_IMAGE) \ $(ACCEPTANCE_CMD) @echo "\n*** test note: you should see grype spit out a 'x509: certificate signed by unknown authority' error --this is expected ***" .PHONY: busybox-1.36 busybox-1.36: $(call title,busybox-1.36 - build environment) docker build -t $(BUSYBOX_IMAGE) -f $(ENVS)/Dockerfile-busybox-1.36 . ## For CI ######################################################## .PHONY: cache.fingerprint cache.fingerprint: $(call title,Install test fixture fingerprint) @find ./environments/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo "$(INSTALL_TEST_CACHE_BUSTER)" >> cache.fingerprint ================================================ FILE: test/install/environments/Dockerfile-alpine-3.6 ================================================ FROM alpine:3.6 RUN apk update && apk add python3 wget curl unzip make ca-certificates RUN curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64" && \ mv cosign-linux-amd64 /usr/local/bin/cosign && \ chmod +x /usr/local/bin/cosign ================================================ FILE: test/install/environments/Dockerfile-busybox-1.36 ================================================ FROM alpine as certs RUN apk update && apk add ca-certificates # note: using qemu with a multi-arch image results in redirects not working with wget # so let docker pull the image that matches the hosts architecture first and then pull the correct asset FROM busybox:1.36.1-musl RUN ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then \ COSIGN_ARCH="amd64"; \ elif [ "$ARCH" = "aarch64" ]; then \ COSIGN_ARCH="arm64"; \ else \ echo "Unsupported architecture: $ARCH" && exit 1; \ fi && \ echo "Downloading cosign for $COSIGN_ARCH" && \ wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-${COSIGN_ARCH} && \ mv cosign-linux-${COSIGN_ARCH} /bin/cosign && \ chmod +x /bin/cosign COPY --from=certs /etc/ssl/certs /etc/ssl/certs ================================================ FILE: test/install/environments/Dockerfile-ubuntu-20.04 ================================================ FROM --platform=linux/amd64 ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba RUN apt update -y && apt install make python3 curl unzip -y RUN LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d : -f2 | tr -d "v\", ") && \ curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb" && \ dpkg -i cosign_${LATEST_VERSION}_amd64.deb ================================================ FILE: test/install/github_test.sh ================================================ . test_harness.sh # check that we can extract single json values test_extract_json_value() { fixture=./testdata/github-api-grype-v0.32.0-release.json content=$(cat ${fixture}) actual=$(extract_json_value "${content}" "tag_name") assertEquals "v0.32.0" "${actual}" "unable to find tag_name" actual=$(extract_json_value "${content}" "id") assertEquals "57501596" "${actual}" "unable to find tag_name" } run_test_case test_extract_json_value # check that we can extract github release tag from github api json test_github_release_tag() { fixture=./testdata/github-api-grype-v0.32.0-release.json content=$(cat ${fixture}) actual=$(github_release_tag "${content}") assertEquals "v0.32.0" "${actual}" "unable to find release tag" } run_test_case test_github_release_tag # download a known good github release checksums and compare against a test-fixture test_download_github_release_checksums() { tmpdir=$(mktemp -d) tag=v0.32.0 github_download="https://github.com/anchore/grype/releases/download/${tag}" name=${PROJECT_NAME} version=$(tag_to_version "${tag}") actual_filepath=$(download_github_release_checksums "${github_download}" "${name}" "${version}" "${tmpdir}") assertFilesEqual \ "./testdata/grype_0.32.0_checksums.txt" \ "${actual_filepath}" \ "unable to find release tag" rm -rf -- "$tmpdir" } run_test_case test_download_github_release_checksums # download a checksums file from a locally served-up snapshot directory and compare against the file in the snapshot dir test_download_github_release_checksums_snapshot() { tmpdir=$(mktemp -d) github_download=$(snapshot_download_url) name=${PROJECT_NAME} version=$(snapshot_version) actual_filepath=$(download_github_release_checksums "${github_download}" "${name}" "${version}" "${tmpdir}") assertFilesEqual \ "$(snapshot_checksums_path)" \ "${actual_filepath}" \ "unable to find release tag" rm -rf -- "$tmpdir" } run_test_case_with_snapshot_release test_download_github_release_checksums_snapshot ================================================ FILE: test/install/test_harness.sh ================================================ # disable using the install.sh entrypoint such that we can unit test # script functions without invoking main() TEST_INSTALL_SH=true . ../../install.sh set -u echoerr() { echo "$@" 1>&2 } printferr() { printf "%s" "$*" >&2 } assertTrue() { if eval "$1"; then echo "assertTrue failed: $2" exit 2 fi } assertFalse() { if eval "$1"; then echo "assertFalse failed: $2" exit 2 fi } assertEquals() { want=$1 got=$2 msg=$3 if [ "$want" != "$got" ]; then echo "assertEquals failed: want='$want' got='$got' $msg" exit 2 fi } assertFilesDoesNotExist() { path="$1" msg=$2 if [ -f "${path}" ]; then echo "assertFilesDoesNotExist failed: path exists '$path': $msg" exit 2 fi } assertFileExists() { path="$1" msg=$2 if [ ! -f "${path}" ]; then echo "assertFileExists failed: path does not exist '$path': $msg" exit 2 fi } assertFilesEqual() { want=$1 got=$2 msg=$3 diff "$1" "$2" if [ $? -ne 0 ]; then echo "assertFilesEqual failed: $msg" exit 2 fi } assertNotEquals() { want=$1 got=$2 msg=$3 if [ "$want" = "$got" ]; then echo "assertNotEquals failed: want='$want' got='$got' $msg" exit 2 fi } log_test_case() { echo " running $@" } run_test_case_with_snapshot_release() { log_test_case ${@:1} worker_pid=$(setup_snapshot_server) trap "teardown_snapshot_server $worker_pid" EXIT # run test function with all arguments ${@:1} trap - EXIT teardown_snapshot_server "${worker_pid}" } serve_port=8000 setup_snapshot_server() { # if you want to see proof in the logs, feel free to adjust the redirection python3 -m http.server --directory "$(snapshot_dir)" $serve_port &> /dev/null & worker_pid=$! echoerr "serving up $(snapshot_dir) on port $serve_port" echoerr "$(ls -1 $(snapshot_dir) | sed 's/^/ ▕―― /')" check_snapshots_server_ready echoerr "snapshot server ready! (worker=${worker_pid})" echo "$worker_pid" } check_snapshots_server_ready() { i=0 until $(curl -m 3 --output /dev/null --silent --head --fail localhost:$serve_port/); do sleep 1 ((i=i+1)) if [ "$i" -gt "30" ]; then echoerr "could not connect to local snapshot server! bailing..." exit 1 fi printferr '.' done } teardown_snapshot_server() { worker_pid="$1" echoerr "stopping worker=${worker_pid}" kill "$worker_pid" } snapshot_version() { partial=$(ls ../../snapshot/*_checksums.txt | grep -o "_.*_checksums.txt") partial="${partial%_checksums.txt}" echo "${partial#_}" } snapshot_download_url() { echo "localhost:${serve_port}" } snapshot_dir() { echo "../../snapshot" } snapshot_checksums_path() { echo "$(ls $(snapshot_dir)/*_checksums.txt)" } snapshot_assets_count() { # example output before wc -l: # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.deb # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.rpm # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.deb # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.rpm # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.zip # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_windows_amd64.zip # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.zip # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz echo "$(find ../../snapshot -maxdepth 1 -type f | grep 'grype_' | grep -v checksums | wc -l | tr -d '[:space:]')" } snapshot_assets_archive_count() { # example output before wc -l: # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.zip # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_windows_amd64.zip # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.zip # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz # ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz echo "$(find ../../snapshot -maxdepth 1 -type f | grep 'grype_' | grep 'tar\|zip' | wc -l | tr -d '[:space:]')" } run_test_case() { log_test_case ${@:1} ${@:1} } ================================================ FILE: test/install/testdata/assets/invalid/.gitignore ================================================ !grype_0.78.0_linux_arm64.tar.gz ================================================ FILE: test/install/testdata/assets/invalid/checksums.txt ================================================ cb4f335e106532b927dac14d4857b7be2333ec1b8bd2aea82be3f9112bb2728f grype_0.78.0_darwin_amd64.tar.gz 51249ee801b41272218252af2c72a644a7ef037b0b27d7b0eae3b55361e82cf6 grype_0.78.0_darwin_arm64.tar.gz cc3cf4fcc856898fcd05ba2b8590de06e380b958fea5957b0a3e4eff5e8aeeaf grype_0.78.0_linux_amd64.deb 3a9af0f08d1aaf15853f8292be0aa896639e09328416a50d5deaefef894bab61 grype_0.78.0_linux_amd64.rpm 6037fd3763b6112302b98db559bb5390fbb06f0011c0585a4be03ca851daa838 grype_0.78.0_linux_amd64.tar.gz 0f2e3e07be5b5eb08637ac9071f4b0f95f8b4c7c7ea66592852ca82fea4adb93 grype_0.78.0_linux_arm64.deb 89a7f68676a18eb9dc0b706036dacbfb8b78833ed0950b8c6fa63ac159b93781 grype_0.78.0_linux_arm64.rpm 0d560e860d6f68cf23c8f0df1f5124ef6d3d8d57abb57a0dae3f951b7772822e grype_0.78.0_linux_arm64.tar.gz 7b22795114e27c3d147998edc9e803988d7c987cad2623d7fb1d7bf730b4e176 grype_0.78.0_linux_ppc64le.deb 91813ac66ad2ef761ce9629eb4213988de594abd4cab9148a85d71bfa80f6699 grype_0.78.0_linux_ppc64le.rpm cb923a08fb9f367410190675f187b6aa5a04c1d538f055700c89c8350b826dcb grype_0.78.0_linux_ppc64le.tar.gz 4beb9d31d61df6212c3f996fc8f33239520eeea083dbe70b0969f23739d44dd1 grype_0.78.0_linux_s390x.deb 28f723777b1a136d2fadbdca0ae5e7e9b26f9bd08114095dbd2898def7e8b0b6 grype_0.78.0_linux_s390x.rpm 6c3e7e54ce40aa33ca5fc774c3be664fb910a99aa77b1e5e3cee77156e8399f4 grype_0.78.0_linux_s390x.tar.gz 31ca5d02a75dbb8f3361ac9836a2384013a67a7d9e2e437cb80e4ddfbd4c7812 grype_0.78.0_windows_amd64.zip ================================================ FILE: test/install/testdata/assets/valid/.gitignore ================================================ !grype_0.78.0_linux_arm64.tar.gz ================================================ FILE: test/install/testdata/assets/valid/checksums.txt ================================================ cb4f335e106532b927dac14d4857b7be2333ec1b8bd2aea82be3f9112bb2728f grype_0.78.0_darwin_amd64.tar.gz 51249ee801b41272218252af2c72a644a7ef037b0b27d7b0eae3b55361e82cf6 grype_0.78.0_darwin_arm64.tar.gz cc3cf4fcc856898fcd05ba2b8590de06e380b958fea5957b0a3e4eff5e8aeeaf grype_0.78.0_linux_amd64.deb 3a9af0f08d1aaf15853f8292be0aa896639e09328416a50d5deaefef894bab61 grype_0.78.0_linux_amd64.rpm 6037fd3763b6112302b98db559bb5390fbb06f0011c0585a4be03ca851daa838 grype_0.78.0_linux_amd64.tar.gz 0f2e3e07be5b5eb08637ac9071f4b0f95f8b4c7c7ea66592852ca82fea4adb93 grype_0.78.0_linux_arm64.deb 89a7f68676a18eb9dc0b706036dacbfb8b78833ed0950b8c6fa63ac159b93781 grype_0.78.0_linux_arm64.rpm 8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d grype_0.78.0_linux_arm64.tar.gz 7b22795114e27c3d147998edc9e803988d7c987cad2623d7fb1d7bf730b4e176 grype_0.78.0_linux_ppc64le.deb 91813ac66ad2ef761ce9629eb4213988de594abd4cab9148a85d71bfa80f6699 grype_0.78.0_linux_ppc64le.rpm cb923a08fb9f367410190675f187b6aa5a04c1d538f055700c89c8350b826dcb grype_0.78.0_linux_ppc64le.tar.gz 4beb9d31d61df6212c3f996fc8f33239520eeea083dbe70b0969f23739d44dd1 grype_0.78.0_linux_s390x.deb 28f723777b1a136d2fadbdca0ae5e7e9b26f9bd08114095dbd2898def7e8b0b6 grype_0.78.0_linux_s390x.rpm 6c3e7e54ce40aa33ca5fc774c3be664fb910a99aa77b1e5e3cee77156e8399f4 grype_0.78.0_linux_s390x.tar.gz 31ca5d02a75dbb8f3361ac9836a2384013a67a7d9e2e437cb80e4ddfbd4c7812 grype_0.78.0_windows_amd64.zip ================================================ FILE: test/install/testdata/github-api-grype-v0.32.0-release.json ================================================ {"id":57501596,"tag_name":"v0.32.0","update_url":"/anchore/grype/releases/tag/v0.32.0","update_authenticity_token":"7XbNZgRHpbHegdv-xRlbe84Y983YgyXa3YKWwv_e0ocqTHagsHq5dxCTQUQnuX3vbsgdWQU3A3__hkVNhKGHSg","delete_url":"/anchore/grype/releases/tag/v0.32.0","delete_authenticity_token":"6tLaRtXKUc-zz4tHIwCbbD7CksxIHK5imZE1gnA39oVCe6fYux5a8cPD9J52kGUzM1Hs9JPBjceG7yyszBk_2A","edit_url":"/anchore/grype/releases/edit/v0.32.0"} ================================================ FILE: test/install/testdata/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt ================================================ 250dddf3338d34012b55b4167b72f8bc87944e61aee35879342206a115a0f64b grype_0.32.0-SNAPSHOT-d461f63_darwin_amd64.tar.gz 4b2973604085c14bc4c452f5354110384d371f0d5c3f93c0e3a44498f54283d7 grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.rpm 569b040bde6d369b9e3b96fb3d9d7ee5aa11267f3aa91fad3d8f4095f1cee150 grype_0.32.0-SNAPSHOT-d461f63_darwin_arm64.tar.gz 5c666286bca9d8c84f7355d5afe720186b0a06bed23ac0518a35a79ff905de28 grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.tar.gz dd1d7492e7a7db9a765a02927b0d019d8f9facb1173ae7c245cd06fefedddfd0 grype_0.32.0-SNAPSHOT-d461f63_windows_amd64.zip dd4e5857856b4655511a75911fd7b53a3ebb9d2f584ae3c7ff7f52ad0dd93745 grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.tar.gz dfe9d8212def2eb3685bacf3c77f664830680a475eb6356e67c96abe4af00e74 grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.rpm e1efed13fa93c207b773cbc2a9252b87049e1a826bacb77b756a20a13a29e465 grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.deb ef2725de0e154059fb59c6268e68fd0ba3a7ce5b23e604166140f284b54ef9b4 grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.deb ================================================ FILE: test/install/testdata/grype_0.32.0_checksums.txt ================================================ 250dddf3338d34012b55b4167b72f8bc87944e61aee35879342206a115a0f64b grype_0.32.0_darwin_amd64.tar.gz 4b2973604085c14bc4c452f5354110384d371f0d5c3f93c0e3a44498f54283d7 grype_0.32.0_linux_amd64.rpm 569b040bde6d369b9e3b96fb3d9d7ee5aa11267f3aa91fad3d8f4095f1cee150 grype_0.32.0_darwin_arm64.tar.gz 5c666286bca9d8c84f7355d5afe720186b0a06bed23ac0518a35a79ff905de28 grype_0.32.0_linux_arm64.tar.gz dd1d7492e7a7db9a765a02927b0d019d8f9facb1173ae7c245cd06fefedddfd0 grype_0.32.0_windows_amd64.zip dd4e5857856b4655511a75911fd7b53a3ebb9d2f584ae3c7ff7f52ad0dd93745 grype_0.32.0_linux_amd64.tar.gz dfe9d8212def2eb3685bacf3c77f664830680a475eb6356e67c96abe4af00e74 grype_0.32.0_linux_arm64.rpm e1efed13fa93c207b773cbc2a9252b87049e1a826bacb77b756a20a13a29e465 grype_0.32.0_linux_arm64.deb ef2725de0e154059fb59c6268e68fd0ba3a7ce5b23e604166140f284b54ef9b4 grype_0.32.0_linux_amd64.deb ================================================ FILE: test/integration/compare_sbom_input_vs_lib_test.go ================================================ package integration import ( "fmt" "os" "testing" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal/log" "github.com/anchore/syft/syft/format/spdxjson" "github.com/anchore/syft/syft/format/spdxtagvalue" "github.com/anchore/syft/syft/format/syftjson" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) func getLatestURL() string { if value, ok := os.LookupEnv("GRYPE_DB_UPDATE_URL"); ok { return value } return distribution.DefaultConfig().LatestURL } func must(e sbom.FormatEncoder, err error) sbom.FormatEncoder { if err != nil { panic(err) } return e } func TestCompareSBOMInputToLibResults(t *testing.T) { // get a grype DB store, status, err := grype.LoadVulnerabilityDB(distribution.Config{ LatestURL: getLatestURL(), }, installation.Config{ DBRootDir: "testdata/grype-db", ValidateChecksum: false, }, true) assert.NoError(t, err) defer log.CloseAndLogError(store, status.Path) definedPkgTypes := strset.New() for _, p := range syftPkg.AllPkgs { definedPkgTypes.Add(string(p)) } // exceptions: rust, php, dart, msrc (kb), etc. are not under test definedPkgTypes.Remove( string(syftPkg.BinaryPkg), // these are removed due to overlap-by-file-ownership string(syftPkg.BitnamiPkg), string(syftPkg.PhpPeclPkg), string(syftPkg.PhpPearPkg), string(syftPkg.RustPkg), string(syftPkg.KbPkg), string(syftPkg.DartPubPkg), string(syftPkg.DotnetPkg), string(syftPkg.PhpComposerPkg), string(syftPkg.ConanPkg), string(syftPkg.CondaPkg), string(syftPkg.HexPkg), string(syftPkg.PortagePkg), string(syftPkg.HomebrewPkg), string(syftPkg.CocoapodsPkg), string(syftPkg.HackagePkg), string(syftPkg.NixPkg), string(syftPkg.JenkinsPluginPkg), // package type cannot be inferred for all formats string(syftPkg.LinuxKernelPkg), string(syftPkg.LinuxKernelModulePkg), string(syftPkg.ModelPkg), string(syftPkg.OpamPkg), string(syftPkg.Rpkg), string(syftPkg.SwiplPackPkg), string(syftPkg.SwiftPkg), string(syftPkg.GithubActionPkg), string(syftPkg.GithubActionWorkflowPkg), string(syftPkg.GraalVMNativeImagePkg), string(syftPkg.ErlangOTPPkg), string(syftPkg.WordpressPluginPkg), // TODO: remove me when there is a matcher for this merged in https://github.com/anchore/grype/pull/1553 string(syftPkg.LuaRocksPkg), string(syftPkg.TerraformPkg), ) observedPkgTypes := strset.New() testCases := []struct { name string image string format sbom.FormatEncoder // knownFormatLossMatches lists match keys (vuln-pkg-version) that are expected to // appear only in results from this SBOM format (and not from an image scan) due to // metadata loss during format encoding. For example, SPDX does not preserve APK file // ownership metadata, so distro-package-fixed ignore rules cannot be scoped to owned // paths and are not emitted — causing matches that the image scan correctly suppresses // to remain visible. This does NOT apply to syft-json, which preserves all metadata. knownFormatLossMatches []string }{ { image: "anchore/test_images:vulnerabilities-alpine", format: syftjson.NewFormatEncoder(), name: "alpine-syft-json", }, { image: "anchore/test_images:vulnerabilities-alpine", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "alpine-spdx-json", }, { image: "anchore/test_images:vulnerabilities-alpine", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "alpine-spdx-tag-value", }, { image: "anchore/test_images:gems", format: syftjson.NewFormatEncoder(), name: "gems-syft-json", }, { image: "anchore/test_images:gems", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "gems-spdx-json", // SPDX does not preserve APK file ownership metadata, so distro-package-fixed // ignore rules cannot be scoped to owned paths and are not emitted from the SBOM. knownFormatLossMatches: []string{"GHSA-8cr8-4vfw-mr7h-rexml-3.2.3.1"}, }, { image: "anchore/test_images:gems", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "gems-spdx-tag-value", // SPDX does not preserve APK file ownership metadata, so distro-package-fixed // ignore rules cannot be scoped to owned paths and are not emitted from the SBOM. knownFormatLossMatches: []string{"GHSA-8cr8-4vfw-mr7h-rexml-3.2.3.1"}, }, { image: "anchore/test_images:vulnerabilities-debian", format: syftjson.NewFormatEncoder(), name: "debian-syft-json", }, { image: "anchore/test_images:vulnerabilities-debian", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "debian-spdx-json", }, { image: "anchore/test_images:vulnerabilities-debian", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "debian-spdx-tag-value", }, { image: "anchore/test_images:vulnerabilities-centos", format: syftjson.NewFormatEncoder(), name: "centos-syft-json", }, { image: "anchore/test_images:vulnerabilities-centos", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "centos-spdx-json", }, { image: "anchore/test_images:vulnerabilities-centos", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "centos-spdx-tag-value", }, { image: "anchore/test_images:npm", format: syftjson.NewFormatEncoder(), name: "npm-syft-json", }, { image: "anchore/test_images:npm", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "npm-spdx-json", }, { image: "anchore/test_images:npm", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "npm-spdx-tag-value", }, { image: "anchore/test_images:java", format: syftjson.NewFormatEncoder(), name: "java-syft-json", }, { image: "anchore/test_images:java", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "java-spdx-json", }, { image: "anchore/test_images:java", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "java-spdx-tag-value", }, { image: "anchore/test_images:golang-56d52bc", format: syftjson.NewFormatEncoder(), name: "go-syft-json", }, { image: "anchore/test_images:golang-56d52bc", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "go-spdx-json", }, { image: "anchore/test_images:golang-56d52bc", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "go-spdx-tag-value", }, { image: "anchore/test_images:arch", format: syftjson.NewFormatEncoder(), name: "arch-syft-json", }, { image: "anchore/test_images:arch", format: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())), name: "arch-spdx-json", }, { image: "anchore/test_images:arch", format: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())), name: "arch-spdx-tag-value", }, } for _, tc := range testCases { imageArchive := PullThroughImageCache(t, tc.image) t.Run(tc.name, func(t *testing.T) { // get SBOM from syft, write to temp file sbomBytes := getSyftSBOM(t, imageArchive, "docker-archive", tc.format) sbomFile, err := os.CreateTemp("", "") assert.NoError(t, err) t.Cleanup(func() { assert.NoError(t, os.Remove(sbomFile.Name())) }) _, err = sbomFile.WriteString(sbomBytes) assert.NoError(t, err) assert.NoError(t, sbomFile.Close()) // get vulns (sbom) matchesFromSbom, _, pkgsFromSbom, err := grype.FindVulnerabilities(store, fmt.Sprintf("sbom:%s", sbomFile.Name()), source.SquashedScope, nil) assert.NoError(t, err) // get vulns (image) imageSource := fmt.Sprintf("docker-archive:%s", imageArchive) matchesFromImage, _, _, err := grype.FindVulnerabilities(store, imageSource, source.SquashedScope, nil) assert.NoError(t, err) // compare packages (shallow) matchSetFromSbom := getMatchSet(matchesFromSbom) matchSetFromImage := getMatchSet(matchesFromImage) sbomOnly := strset.Difference(matchSetFromSbom, matchSetFromImage) if len(tc.knownFormatLossMatches) > 0 { sbomOnly.Remove(tc.knownFormatLossMatches...) } assert.Empty(t, sbomOnly.List(), "vulnerabilities present only in results when using sbom as input") assert.Empty(t, strset.Difference(matchSetFromImage, matchSetFromSbom).List(), "vulnerabilities present only in results when using image as input") // track all covered package types (for use after the test) for _, p := range pkgsFromSbom { observedPkgTypes.Add(string(p.Type)) } }) } // ensure we've covered all package types (-rust, -kb) unobservedPackageTypes := strset.Difference(definedPkgTypes, observedPkgTypes) assert.Empty(t, unobservedPackageTypes.List(), "not all package type were covered in testing") } ================================================ FILE: test/integration/db_mock_test.go ================================================ package integration import ( "github.com/anchore/grype/grype/version" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/grype/vulnerability/mock" "github.com/anchore/syft/syft/cpe" ) func newMockDbProvider() vulnerability.Provider { return mock.VulnerabilityProvider([]vulnerability.Vulnerability{ { Reference: vulnerability.Reference{ ID: "CVE-jdk", Namespace: "nvd:cpe", }, PackageName: "jdk", Constraint: version.MustGetConstraint("< 1.8.0_401", version.JVMFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-2024-0000", Namespace: "nvd:cpe", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.10", version.UnknownFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:lib_vnc_project-(server):libvncserver:*:*:*:*:*:*:*:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-bogus-my-package-1", Namespace: "nvd:cpe", }, PackageName: "my-package", Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-bogus-my-package-2-never-match", Namespace: "nvd:cpe", }, PackageName: "my-package", Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat), CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:something-wrong:my-package:*:*:*:*:*:*:something:*", "")}, }, { Reference: vulnerability.Reference{ ID: "CVE-2024-0000", Namespace: "alpine:distro:alpine:3.12", }, PackageName: "libvncserver", Constraint: version.MustGetConstraint("< 0.9.10", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-azure-autorest-vuln-false-positive", Namespace: "alpine:distro:alpine:3.12", }, PackageName: "ko", Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-npm-false-positive-in-apk-subpackage", Namespace: "alpine:distro:alpine:3.12", }, PackageName: "npm-apk-package-with-false-positive", Constraint: version.MustGetConstraint("< 0", version.ApkFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-gentoo-skopeo", Namespace: "gentoo:distro:gentoo:2.8", }, PackageName: "app-containers/skopeo", Constraint: version.MustGetConstraint("< 1.6.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-coverage-main-module-vuln", Namespace: "github:language:go", }, PackageName: "github.com/anchore/coverage", Constraint: version.MustGetConstraint("< 1.4.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-uuid-vuln", Namespace: "github:language:go", }, PackageName: "github.com/google/uuid", Constraint: version.MustGetConstraint("< 1.4.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-azure-autorest-vuln-false-positive", Namespace: "github:language:go", }, PackageName: "github.com/azure/go-autorest/autorest", Constraint: version.MustGetConstraint("< 0.11.30", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-bogus-my-package-2-idris", Namespace: "github:language:idris", }, PackageName: "my-package", Constraint: version.MustGetConstraint("< 2.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-javascript-validator", Namespace: "github:language:javascript", }, PackageName: "npm", Constraint: version.MustGetConstraint("> 5, < 7.2.1", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-npm-false-positive-in-apk-subpackage", Namespace: "github:language:javascript", }, PackageName: "npm-apk-subpackage-with-false-positive", Constraint: version.MustGetConstraint("< 2.0.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-python-pygments", Namespace: "github:language:python", }, PackageName: "pygments", Constraint: version.MustGetConstraint("< 2.6.2", version.PythonFormat), }, //{ // Reference: vulnerability.Reference{ // ID: "CVE-my-package-python", // Namespace: "github:language:python", // }, // PackageName: "my-package", //}, { Reference: vulnerability.Reference{ ID: "CVE-ruby-bundler", Namespace: "github:language:ruby", // github:language:gem ?? }, PackageName: "bundler", Constraint: version.MustGetConstraint("> 2.0.0, <= 2.1.4", version.UnknownFormat), //version.GemFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-java-example-java-app", Namespace: "github:language:java", }, PackageName: "org.anchore:example-java-app-maven", Constraint: version.MustGetConstraint(">= 0.0.1, < 1.2.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-dotnet-sample", Namespace: "github:language:dotnet", }, PackageName: "awssdk.core", Constraint: version.MustGetConstraint(">= 3.7.0.0, < 3.7.12.0", version.UnknownFormat), // was: "dotnet" }, { Reference: vulnerability.Reference{ ID: "CVE-haskell-sample", Namespace: "github:language:haskell", }, PackageName: "shellcheck", Constraint: version.MustGetConstraint("< 0.9.0", version.UnknownFormat), // was: "haskell" }, { Reference: vulnerability.Reference{ ID: "CVE-hex-plug", Namespace: "github:language:elixir", }, PackageName: "plug", Constraint: version.MustGetConstraint("< 1.12.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-rust-sample-1", Namespace: "github:language:rust", }, PackageName: "hello-auditable", Constraint: version.MustGetConstraint("< 0.2.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-rust-sample-2", Namespace: "github:language:rust", }, PackageName: "auditable", Constraint: version.MustGetConstraint("< 0.2.0", version.UnknownFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-dpkg-apt", Namespace: "debian:distro:debian:8", }, PackageName: "apt-dev", Constraint: version.MustGetConstraint("<= 1.8.2", version.DebFormat), // was: "dpkg" }, { Reference: vulnerability.Reference{ ID: "CVE-rpmdb-dive", Namespace: "redhat:distro:redhat:8", }, PackageName: "dive", Constraint: version.MustGetConstraint("<= 1.0.42", version.RpmFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-2016-3333", Namespace: "msrc:distro:windows:10816", }, PackageName: "10816", Constraint: version.MustGetConstraint("3200970 || 878787 || base", version.KBFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-rpmdb-dive", Namespace: "sles:distro:sles:12.5", }, PackageName: "dive", Constraint: version.MustGetConstraint("<= 1.0.42", version.RpmFormat), }, { Reference: vulnerability.Reference{ ID: "CVE-arch-xz-backdoor", Namespace: "arch:distro:arch:rolling", }, PackageName: "xz", Constraint: version.MustGetConstraint("< 5.6.1-2", version.PacmanFormat), }, }...) } ================================================ FILE: test/integration/match_by_image_test.go ================================================ package integration import ( "context" "sort" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher" "github.com/anchore/grype/grype/matcher/dotnet" "github.com/anchore/grype/grype/matcher/golang" "github.com/anchore/grype/grype/matcher/java" "github.com/anchore/grype/grype/matcher/javascript" "github.com/anchore/grype/grype/matcher/python" "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/rust" "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/search" "github.com/anchore/grype/grype/vex" vexStatus "github.com/anchore/grype/grype/vex/status" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/stringutil" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/cataloging/pkgcataloging" "github.com/anchore/syft/syft/cpe" syftPkg "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) func addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/lib/apk/db/installed") if len(packages) != 3 { t.Logf("Alpine Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (alpine)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("alpine:distro:alpine:3.12"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ // note: we are matching on the secdb record, not NVD primarily Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "alpine", Version: "3.12.0", }, Namespace: "alpine:distro:alpine:3.12", Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, Found: match.DistroResult{ VersionConstraint: "< 0.9.10 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.ApkMatcher, }, { // note: the input pURL has an upstream reference (redundant) Type: match.ExactIndirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "alpine", Version: "3.12.0", }, Namespace: "alpine:distro:alpine:3.12", Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, Found: match.DistroResult{ VersionConstraint: "< 0.9.10 (unknown)", VulnerabilityID: "CVE-2024-0000", }, Matcher: match.ApkMatcher, Confidence: 1, }, }, }) } func addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/javascript/pkg-json/package.json") if len(packages) != 1 { t.Logf("Javascript Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (javascript)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:javascript"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "javascript", Namespace: "github:language:javascript", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: "> 5, < 7.2.1 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.JavascriptMatcher, }, }, }) } func addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/python/dist-info/METADATA") if len(packages) != 1 { for _, p := range packages { t.Logf("Python Package: %s %+v", p.ID(), p) } t.Fatalf("problem with upstream syft cataloger (python)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:python"), search.ByPackageName(strings.ToLower(thePkg.Name))) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "python", Namespace: "github:language:python", Package: match.PackageParameter{ Name: provider.PackageSearchNames(thePkg)[0], // there is normalization that should be accounted for Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: "< 2.6.2 (python)", VulnerabilityID: vulnObj.ID, }, Matcher: match.PythonMatcher, }, }, }) } func addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/dotnet/TestLibrary.deps.json") // 55caef8df7ac822e Pkg(name="TestLibrary" version="1.0.0" type="dotnet" id="55caef8df7ac822e") // 0012329cdebba0ea Pkg(name="AWSSDK.Core" version="3.7.10.6" type="dotnet" id="0012329cdebba0ea") // 07ec6fb2adb2cf8f Pkg(name="Microsoft.Extensions.DependencyInjection.Abstractions" version="6.0.0" type="dotnet" id="07ec6fb2adb2cf8f") // ff03e77b91acca32 Pkg(name="Microsoft.Extensions.DependencyInjection" version="6.0.0" type="dotnet" id="ff03e77b91acca32") // a1ea42c8f064083e Pkg(name="Microsoft.Extensions.Logging.Abstractions" version="6.0.0" type="dotnet" id="a1ea42c8f064083e") // aaef85a2649e5d15 Pkg(name="Microsoft.Extensions.Logging" version="6.0.0" type="dotnet" id="aaef85a2649e5d15") // 4af0fb6a81ba0423 Pkg(name="Microsoft.Extensions.Options" version="6.0.0" type="dotnet" id="4af0fb6a81ba0423") // cb41a8aefdf40c3a Pkg(name="Microsoft.Extensions.Primitives" version="6.0.0" type="dotnet" id="cb41a8aefdf40c3a") // 5ee80fba9caa3ab3 Pkg(name="Newtonsoft.Json" version="13.0.1" type="dotnet" id="5ee80fba9caa3ab3") // df4b5dc73acd1f36 Pkg(name="Serilog.Sinks.Console" version="4.0.1" type="dotnet" id="df4b5dc73acd1f36") // 023b9ba74c5c5ef5 Pkg(name="Serilog" version="2.10.0" type="dotnet" id="023b9ba74c5c5ef5") // 430e4d4304a3ff55 Pkg(name="System.Diagnostics.DiagnosticSource" version="6.0.0" type="dotnet" id="430e4d4304a3ff55") // 42021023d8f87661 Pkg(name="System.Runtime.CompilerServices.Unsafe" version="6.0.0" type="dotnet" id="42021023d8f87661") // 2bb01d8c22df1e95 Pkg(name="TestCommon" version="1.0.0" type="dotnet" id="2bb01d8c22df1e95") if len(packages) != 14 { for _, p := range packages { t.Logf("Dotnet Package: %s %+v", p.ID(), p) } t.Fatalf("problem with upstream syft cataloger (dotnet)") } thePkg := pkg.New(packages[1]) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:dotnet"), search.ByPackageName(strings.ToLower(thePkg.Name))) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "dotnet", Namespace: "github:language:dotnet", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: ">= 3.7.0.0, < 3.7.12.0 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.DotnetMatcher, }, }, }) } func addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/ruby/specifications/bundler.gemspec") if len(packages) != 1 { t.Logf("Ruby Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (ruby)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:ruby"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "ruby", Namespace: "github:language:ruby", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: "> 2.0.0, <= 2.1.4 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.RubyGemMatcher, }, }, }) } func addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { modPackages := catalog.PackagesByPath("/golang/go.mod") if len(modPackages) != 1 { t.Logf("Golang Mod Packages: %+v", modPackages) t.Fatalf("problem with upstream syft cataloger (golang)") } binPackages := catalog.PackagesByPath("/go-app") // contains 2 package + a single stdlib package if len(binPackages) != 3 { t.Logf("Golang Bin Packages: %+v", binPackages) t.Fatalf("problem with upstream syft cataloger (golang)") } var packages []syftPkg.Package packages = append(packages, modPackages...) packages = append(packages, binPackages...) for _, p := range packages { // no vuln match supported for main module if p.Name == "github.com/anchore/coverage" { continue } if p.Name == "stdlib" { continue } thePkg := pkg.New(p) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:go"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "go", Namespace: "github:language:go", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: "< 1.4.0 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.GoModuleMatcher, }, }, }) } } func addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := make([]syftPkg.Package, 0) for p := range catalog.Enumerate(syftPkg.JavaPkg) { packages = append(packages, p) } if len(packages) != 2 { // 2, because there's a nested JAR inside the test fixture JAR t.Logf("Java Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (java)") } theSyftPkg := packages[0] groupId := theSyftPkg.Metadata.(syftPkg.JavaArchive).PomProperties.GroupID lookup := groupId + ":" + theSyftPkg.Name thePkg := pkg.New(theSyftPkg) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:java"), search.ByPackageName(lookup)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "java", Namespace: "github:language:java", Package: match.PackageParameter{ Name: provider.PackageSearchNames(thePkg)[0], // there is normalization that should be accounted for Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: ">= 0.0.1, < 1.2.0 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.JavaMatcher, }, }, }) } func addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/dpkg/status") if len(packages) != 1 { t.Logf("Dpkg Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (dpkg)") } thePkg := pkg.New(packages[0]) // NOTE: this is an indirect match, in typical debian style vulns, err := provider.FindVulnerabilities(byNamespace("debian:distro:debian:8"), search.ByPackageName(thePkg.Name+"-dev")) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactIndirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "debian", Version: "8", }, Namespace: "debian:distro:debian:8", Package: match.PackageParameter{ Name: "apt-dev", Version: "1.8.2", }, }, Found: match.DistroResult{ VersionConstraint: "<= 1.8.2 (deb)", VulnerabilityID: vulnObj.ID, }, Matcher: match.DpkgMatcher, }, }, }) } func addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS") if len(packages) != 1 { t.Logf("Portage Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (portage)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("gentoo:distro:gentoo:2.8"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "gentoo", Version: "2.8", }, Namespace: "gentoo:distro:gentoo:2.8", Package: match.PackageParameter{ Name: "app-containers/skopeo", Version: "1.5.1", }, }, Found: match.DistroResult{ VersionConstraint: "< 1.6.0 (unknown)", VulnerabilityID: vulnObj.ID, }, Matcher: match.PortageMatcher, }, }, }) } func addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/rpm/Packages") if len(packages) != 1 { t.Logf("RPMDB Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (RPMDB)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("redhat:distro:redhat:8"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "centos", Version: "8", }, Namespace: "redhat:distro:redhat:8", Package: match.PackageParameter{ Name: "dive", Version: "0:0.9.2-1", }, }, Found: match.DistroResult{ VersionConstraint: "<= 1.0.42 (rpm)", VulnerabilityID: vulnObj.ID, }, Matcher: match.RpmMatcher, }, }, }) } func addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/rpm/Packages") if len(packages) != 1 { t.Logf("Sles Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (RPMDB)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("redhat:distro:redhat:8"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] vulnObj.Namespace = "sles:distro:sles:12.5" theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "sles", Version: "12.5", }, Namespace: "sles:distro:sles:12.5", Package: match.PackageParameter{ Name: "dive", Version: "0:0.9.2-1", }, }, Found: match.DistroResult{ VersionConstraint: "<= 1.0.42 (rpm)", VulnerabilityID: vulnObj.ID, }, Matcher: match.RpmMatcher, }, }, }) } func addArchMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/var/lib/pacman/local/xz-5.2.4-1/desc") if len(packages) != 1 { t.Logf("Arch Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (pacman)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("arch:distro:arch:rolling"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "archlinux", Version: "", }, Namespace: "arch:distro:arch:rolling", Package: match.PackageParameter{ Name: "xz", Version: "5.2.4-1", }, }, Found: match.DistroResult{ VersionConstraint: "< 5.6.1-2 (pacman)", VulnerabilityID: vulnObj.ID, }, Matcher: match.PacmanMatcher, }, }, }) } func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/haskell/stack.yaml") if len(packages) < 1 { t.Logf("Haskell Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (haskell)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:haskell"), search.ByPackageName(strings.ToLower(thePkg.Name))) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "haskell", Namespace: "github:language:haskell", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: "< 0.9.0 (unknown)", VulnerabilityID: "CVE-haskell-sample", }, Matcher: match.StockMatcher, }, }, }) } func addHexMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/hex/mix.lock") if len(packages) < 1 { t.Logf("Hex Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (elixir-mix-lock)") } thePkg := pkg.New(packages[0]) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:elixir"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "elixir", Namespace: "github:language:elixir", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: "< 1.12.0 (unknown)", VulnerabilityID: "CVE-hex-plug", }, Matcher: match.HexMatcher, }, }, }) } func addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/opt/java/openjdk/release") if len(packages) < 1 { t.Logf("JVM Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (java-jvm-cataloger)") } for _, p := range packages { thePkg := pkg.New(p) vulns, err := provider.FindVulnerabilities(byNamespace("nvd:cpe"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] // why is this being set? vulnObj.CPEs = []cpe.CPE{ cpe.Must("cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", ""), } theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.CPEMatch, Confidence: 0.9, SearchedBy: match.CPEParameters{ Namespace: "nvd:cpe", CPEs: []string{ "cpe:2.3:a:oracle:jdk:1.8.0:update400:*:*:*:*:*:*", }, Package: match.PackageParameter{Name: "jdk", Version: "1.8.0_400-b07"}, }, Found: match.CPEResult{ VulnerabilityID: "CVE-jdk", VersionConstraint: "< 1.8.0_401 (jvm)", CPEs: []string{ "cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*", }, }, Matcher: match.StockMatcher, }, }, }) } } func addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) { packages := catalog.PackagesByPath("/hello-auditable") if len(packages) < 1 { t.Logf("Rust Packages: %+v", packages) t.Fatalf("problem with upstream syft cataloger (cargo-auditable-binary-cataloger)") } for _, p := range packages { thePkg := pkg.New(p) vulns, err := provider.FindVulnerabilities(byNamespace("github:language:rust"), search.ByPackageName(thePkg.Name)) require.NoError(t, err) require.NotEmpty(t, vulns) vulnObj := vulns[0] theResult.Add(match.Match{ Vulnerability: vulnObj, Package: thePkg, Details: []match.Detail{ { Type: match.ExactDirectMatch, Confidence: 1.0, SearchedBy: match.EcosystemParameters{ Language: "rust", Namespace: "github:language:rust", Package: match.PackageParameter{ Name: thePkg.Name, Version: thePkg.Version, }, }, Found: match.EcosystemResult{ VersionConstraint: vulnObj.Constraint.String(), VulnerabilityID: vulnObj.ID, }, Matcher: match.RustMatcher, }, }, }) } } func TestMatchByImage(t *testing.T) { observedMatchers := stringutil.NewStringSet() definedMatchers := stringutil.NewStringSet() for _, l := range match.AllMatcherTypes { definedMatchers.Add(string(l)) } tests := []struct { name string expectedFn func(source.Source, *syftPkg.Collection, vulnerability.Provider) match.Matches }{ { name: "image-debian-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addPythonMatches(t, theSource, catalog, provider, &expectedMatches) addRubyMatches(t, theSource, catalog, provider, &expectedMatches) addJavaMatches(t, theSource, catalog, provider, &expectedMatches) addDpkgMatches(t, theSource, catalog, provider, &expectedMatches) addJavascriptMatches(t, theSource, catalog, provider, &expectedMatches) addDotnetMatches(t, theSource, catalog, provider, &expectedMatches) addGolangMatches(t, theSource, catalog, provider, &expectedMatches) addHaskellMatches(t, theSource, catalog, provider, &expectedMatches) addHexMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, { name: "image-centos-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addRhelMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, { name: "image-alpine-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addAlpineMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, { name: "image-sles-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addSlesMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, { name: "image-arch-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addArchMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, // TODO: add this back in when #744 is fully implemented (see https://github.com/anchore/grype/issues/744#issuecomment-2448163737) //{ // name: "image-portage-match-coverage", // expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { // expectedMatches := match.NewMatches() // addPortageMatches(t, theSource, catalog, provider, &expectedMatches) // return expectedMatches // }, //}, { name: "image-rust-auditable-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addRustMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, { name: "image-jvm-match-coverage", expectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches { expectedMatches := match.NewMatches() addJvmMatches(t, theSource, catalog, provider, &expectedMatches) return expectedMatches }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { theProvider := newMockDbProvider() imagetest.GetFixtureImage(t, "docker-archive", test.name) tarPath := imagetest.GetFixtureImageTarPath(t, test.name) // this is purely done to help setup mocks theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, theSource.Close()) }) // TODO: relationships are not verified at this time // enable all catalogers to cover non default cases config := syft.DefaultCreateSBOMConfig().WithCatalogerSelection(pkgcataloging.NewSelectionRequest().WithDefaults("all")) config.Search.Scope = source.SquashedScope s, err := syft.CreateSBOM(context.Background(), theSource, config) require.NoError(t, err) require.NotNil(t, s) // TODO: we need to use the API default configuration, not something hard coded here matchers := matcher.NewDefaultMatchers(matcher.Config{ Java: java.MatcherConfig{ UseCPEs: true, }, Ruby: ruby.MatcherConfig{ UseCPEs: true, }, Python: python.MatcherConfig{ UseCPEs: true, }, Dotnet: dotnet.MatcherConfig{ UseCPEs: true, }, Javascript: javascript.MatcherConfig{ UseCPEs: true, }, Golang: golang.MatcherConfig{ UseCPEs: true, }, Rust: rust.MatcherConfig{ UseCPEs: true, }, Stock: stock.MatcherConfig{ UseCPEs: true, }, }) actualResults := grype.FindVulnerabilitiesForPackage(theProvider, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{ Distro: pkg.DistroConfig{ Override: distro.FromRelease(s.Artifacts.LinuxDistribution, distro.DefaultFixChannels()), }, })) for _, m := range actualResults.Sorted() { for _, d := range m.Details { observedMatchers.Add(string(d.Matcher)) } } // build expected matches from what's discovered from the catalog expectedMatches := test.expectedFn(theSource, s.Artifacts.Packages, theProvider) assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted()) }) } // Test that VEX matchers produce matches when fed documents with "affected" // or "under investigation" statuses. for n, tc := range map[string]struct { vexStatus vexStatus.Status vexDocuments []string }{ "csaf-affected": {vexStatus.Affected, []string{"testdata/vex/csaf/affected.csaf.json"}}, "csaf-under_investigation": {vexStatus.UnderInvestigation, []string{"testdata/vex/csaf/under_investigation.csaf.json"}}, "openvex-affected": {vexStatus.Affected, []string{"testdata/vex/openvex/affected.openvex.json"}}, "openvex-under_investigation": {vexStatus.UnderInvestigation, []string{"testdata/vex/openvex/under_investigation.openvex.json"}}, } { t.Run(n, func(t *testing.T) { ignoredMatches := testIgnoredMatches() vexedResults := vexMatches(t, ignoredMatches, tc.vexStatus, tc.vexDocuments) if len(vexedResults.Sorted()) != 1 { t.Errorf("expected one vexed result, got none") } expectedMatches := match.NewMatches() // The single match in the actual results is the same in ignoredMatched // but must the details of the VEX matcher appended if len(vexedResults.Sorted()) < 1 { t.Errorf( "Expected VEXed Results to produce an array of vexMatches but got none; len(vexedResults)=%d", len(vexedResults.Sorted()), ) } result := vexedResults.Sorted()[0] if len(result.Details) != len(ignoredMatches[0].Match.Details)+1 { t.Errorf( "Details in VEXed results don't match (expected %d, got %d)", len(ignoredMatches[0].Match.Details)+1, len(result.Details), ) } result.Details = result.Details[:len(result.Details)-1] actualResults := match.NewMatches() actualResults.Add(result) expectedMatches.Add(ignoredMatches[0].Match) assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted()) for _, m := range vexedResults.Sorted() { for _, d := range m.Details { observedMatchers.Add(string(d.Matcher)) } } }) } // ensure that integration test cases stay in sync with the implemented matchers observedMatchers.Remove(string(match.StockMatcher)) definedMatchers.Remove(string(match.StockMatcher)) definedMatchers.Remove(string(match.MsrcMatcher)) definedMatchers.Remove(string(match.PortageMatcher)) // TODO: add this back in when #744 is complete definedMatchers.Remove(string(match.BitnamiMatcher)) // bitnami will be tested via quality gate if len(observedMatchers) != len(definedMatchers) { t.Errorf("matcher coverage incomplete (matchers=%d, coverage=%d)", len(definedMatchers), len(observedMatchers)) defs := definedMatchers.ToSlice() sort.Strings(defs) obs := observedMatchers.ToSlice() sort.Strings(obs) t.Log(cmp.Diff(defs, obs)) } } // testIgnoredMatches returns an list of ignored matches to test the vex // matchers func testIgnoredMatches() []match.IgnoredMatch { return []match.IgnoredMatch{ { Match: match.Match{ Vulnerability: vulnerability.Vulnerability{ Reference: vulnerability.Reference{ ID: "CVE-2024-0000", Namespace: "alpine:distro:alpine:3.12", }, }, Package: pkg.Package{ ID: "44fa3691ae360cac", Name: "libvncserver", Version: "0.9.9", Licenses: []string{"GPL-2.0-or-later"}, Type: "apk", CPEs: []cpe.CPE{ { Attributes: cpe.Attributes{ Part: "a", Vendor: "libvncserver", Product: "libvncserver", Version: "0.9.9", }, }, }, PURL: "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0", Upstreams: []pkg.UpstreamPackage{{Name: "libvncserver"}}, }, Details: []match.Detail{ { Type: match.ExactIndirectMatch, SearchedBy: match.DistroParameters{ Distro: match.DistroIdentification{ Type: "alpine", Version: "3.12.0", }, Namespace: "alpine:distro:alpine:3.12", Package: match.PackageParameter{ Name: "libvncserver", Version: "0.9.9", }, }, Found: match.DistroResult{ VersionConstraint: "< 0.9.10 (unknown)", VulnerabilityID: "CVE-2024-0000", }, Matcher: match.ApkMatcher, Confidence: 1, }, }, }, AppliedIgnoreRules: []match.IgnoreRule{}, }, } } // vexMatches moves the first match of a matches list to an ignore list and // applies a VEX "affected" document to it to move it to the matches list. func vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vexStatus.Status, vexDocuments []string) match.Matches { matches := match.NewMatches() vexMatcher, err := vex.NewProcessor(vex.ProcessorOptions{ Documents: vexDocuments, IgnoreRules: []match.IgnoreRule{ {VexStatus: string(vexStatus)}, }, }) if err != nil { t.Errorf("creating VEX processor: %s", err) } pctx := &pkg.Context{ Source: &source.Description{ Metadata: source.ImageMetadata{ RepoDigests: []string{ "alpine@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, }, }, } vexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches) if err != nil { t.Errorf("applying VEX data: %s", err) } if len(ignoredMatches) != 0 { t.Errorf("VEX text fixture %s must affect all ignored matches (%d left)", vexDocuments, len(ignoredMatches)) } return *vexedMatches } func assertMatches(t *testing.T, expected, actual []match.Match) { t.Helper() opts := []cmp.Option{ cmpopts.EquateEmpty(), cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"), cmpopts.IgnoreFields(pkg.Package{}, "Locations", "Distro"), cmpopts.SortSlices(func(a, b match.Match) bool { return a.Package.ID < b.Package.ID }), } if diff := cmp.Diff(expected, actual, opts...); diff != "" { t.Errorf("mismatch (-want +got):\n%s", diff) } } func byNamespace(ns string) vulnerability.Criteria { return search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) { return v.Reference.Namespace == ns, "", nil }) } ================================================ FILE: test/integration/match_by_sbom_document_test.go ================================================ package integration import ( "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" "github.com/anchore/syft/syft/source" ) func TestMatchBySBOMDocument(t *testing.T) { tests := []struct { name string fixture string expectedIDs []string expectedDetails []match.Detail }{ { name: "unknown package type", fixture: "testdata/sbom/syft-sbom-with-unknown-packages.json", expectedIDs: []string{"CVE-bogus-my-package-2-idris"}, expectedDetails: []match.Detail{ { Type: match.ExactDirectMatch, SearchedBy: match.EcosystemParameters{ Language: "idris", Namespace: "github:language:idris", Package: match.PackageParameter{Name: "my-package", Version: "1.0.5"}, }, Found: match.EcosystemResult{ VersionConstraint: "< 2.0 (unknown)", VulnerabilityID: "CVE-bogus-my-package-2-idris", }, Matcher: match.StockMatcher, Confidence: 1, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { vp := newMockDbProvider() matches, _, _, err := grype.FindVulnerabilities(vp, fmt.Sprintf("sbom:%s", test.fixture), source.SquashedScope, nil) assert.NoError(t, err) details := make([]match.Detail, 0) ids := strset.New() for _, m := range matches.Sorted() { details = append(details, m.Details...) ids.Add(m.Vulnerability.ID) } require.Len(t, details, len(test.expectedDetails)) cmpOpts := []cmp.Option{ cmpopts.IgnoreFields(pkg.Package{}, "Locations"), } for i := range test.expectedDetails { if d := cmp.Diff(test.expectedDetails[i], details[i], cmpOpts...); d != "" { t.Errorf("unexpected match details (-want +got):\n%s", d) } } assert.ElementsMatch(t, test.expectedIDs, ids.List()) }) } } ================================================ FILE: test/integration/testdata/.gitignore ================================================ !**/image-*/Dockerfile grype-db grype-db-download* ================================================ FILE: test/integration/testdata/Makefile ================================================ # change these if you want CI to not use previous stored cache INTEGRATION_CACHE_BUSTER := "894d8ca" .PHONY: cache.fingerprint cache.fingerprint: find image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> cache.fingerprint ================================================ FILE: test/integration/testdata/image-alpine-match-coverage/Dockerfile ================================================ FROM cgr.dev/chainguard/go AS builder FROM scratch COPY . . ================================================ FILE: test/integration/testdata/image-alpine-match-coverage/etc/os-release ================================================ NAME="Alpine Linux" ID=alpine VERSION_ID=3.12.0 PRETTY_NAME="Alpine Linux v3.12" HOME_URL="https://alpinelinux.org/" BUG_REPORT_URL="https://bugs.alpinelinux.org/" ================================================ FILE: test/integration/testdata/image-alpine-match-coverage/lib/apk/db/installed ================================================ C:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k= P:libvncserver V:0.9.9 A:x86_64 S:166239 I:389120 T:Library to make writing a vnc server easy U:http://libvncserver.sourceforge.net/ L:GPL-2.0-or-later o:libvncserver m:A. Wilcox t:1572818861 c:bf1ec813f662f128fc6b70f37ef1c0474bb24488 D:so:libc.musl-x86_64.so.1 so:libgcrypt.so.20 so:libgnutls.so.30 so:libjpeg.so.8 so:libpng16.so.16 so:libz.so.1 p:so:libvncclient.so.1=1.0.0 so:libvncserver.so.1=1.0.0 F:usr F:usr/lib R:libvncclient.so.1 a:0:0:777 Z:Q1quyp/JcSPFQhtQFjMUYdMwRvAWM= R:libvncserver.so.1.0.0 a:0:0:755 Z:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw= R:libvncserver.so.1 a:0:0:777 Z:Q184HrHsxEBqnsH4QNxeU5w8alhKI= R:libvncclient.so.1.0.0 a:0:0:755 Z:Q1IEjCrEwVlQt2GjIsb3o39vcgqMg= C:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k= P:ko V:0.15.1 A:x86_64 S:166239 I:389120 T:Build and deploy Go applications o:ko t:1572818861 R:ko a:0:0:755 Z:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw= C:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k= P:npm-apk-subpackage-with-false-positive V:7.0.0 A:x86_64 S:166239 I:389120 T:NPM package, in an APK subpackage, that has a false positive o:npm-apk-package-with-false-positive t:1572818861 F:lib F:lib/node_modules F:lib/node_modules/npm-apk-subpackage-with-false-positive R:package.json a:0:0:755 Z:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw= ================================================ FILE: test/integration/testdata/image-arch-match-coverage/Dockerfile ================================================ FROM docker.io/archlinux:20191105@sha256:1097437745db73ba839d60b9b9b96e6648e62751519a1319bfccc849f6a3f74c ================================================ FILE: test/integration/testdata/image-centos-match-coverage/Dockerfile ================================================ FROM scratch COPY . . ================================================ FILE: test/integration/testdata/image-centos-match-coverage/etc/os-release ================================================ NAME="CentOS Linux" VERSION="8 (Core)" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="8" PLATFORM_ID="platform:el8" PRETTY_NAME="CentOS Linux 8 (Core)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:centos:centos:8" HOME_URL="https://www.centos.org/" BUG_REPORT_URL="https://bugs.centos.org/" ================================================ FILE: test/integration/testdata/image-centos-match-coverage/var/lib/rpm/generate-fixture.sh ================================================ #!/usr/bin/env bash set -eux docker create --name generate-rpmdb-fixture centos:latest sh -c 'tail -f /dev/null' function cleanup { docker kill generate-rpmdb-fixture docker rm generate-rpmdb-fixture } trap cleanup EXIT docker start generate-rpmdb-fixture docker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF mkdir -p /scratch cd /scratch rpm --initdb --dbpath /scratch curl -sSLO https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm rpm --dbpath /scratch -ivh dive_0.9.2_linux_amd64.rpm rm dive_0.9.2_linux_amd64.rpm rpm --dbpath /scratch -qa EOF docker cp generate-rpmdb-fixture:/scratch/Packages . ================================================ FILE: test/integration/testdata/image-debian-match-coverage/Dockerfile ================================================ FROM docker.io/golang:1.16@sha256:92ccbb6513249c08e582ca3eafc5c9176dbc5cbfe73af245542c5c78250e9b49 WORKDIR /go/src/github.com/anchore/test/ COPY golang/ ./ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-app . FROM scratch COPY --from=0 /go/src/github.com/anchore/test/go-app ./ COPY . . ================================================ FILE: test/integration/testdata/image-debian-match-coverage/dotnet/TestLibrary.deps.json ================================================ { "runtimeTarget": { "name": ".NETCoreApp,Version=v6.0", "signature": "" }, "compilationOptions": {}, "targets": { ".NETCoreApp,Version=v6.0": { "TestLibrary/1.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "6.0.0", "Microsoft.Extensions.Logging": "6.0.0", "Newtonsoft.Json": "13.0.1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.1", "TestCommon": "1.0.0" }, "runtime": { "TestLibrary.dll": {} } }, "AWSSDK.Core/3.7.10.6": { "runtime": { "lib/netcoreapp3.1/AWSSDK.Core.dll": { "assemblyVersion": "3.3.0.0", "fileVersion": "3.7.10.6" } } }, "Microsoft.Extensions.DependencyInjection/6.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", "System.Runtime.CompilerServices.Unsafe": "6.0.0" }, "runtime": { "lib/net6.0/Microsoft.Extensions.DependencyInjection.dll": { "assemblyVersion": "6.0.0.0", "fileVersion": "6.0.21.52210" } } }, "Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0": { "runtime": { "lib/net6.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { "assemblyVersion": "6.0.0.0", "fileVersion": "6.0.21.52210" } } }, "Microsoft.Extensions.Logging/6.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "6.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.0", "Microsoft.Extensions.Options": "6.0.0", "System.Diagnostics.DiagnosticSource": "6.0.0" }, "runtime": { "lib/netstandard2.1/Microsoft.Extensions.Logging.dll": { "assemblyVersion": "6.0.0.0", "fileVersion": "6.0.21.52210" } } }, "Microsoft.Extensions.Logging.Abstractions/6.0.0": { "runtime": { "lib/net6.0/Microsoft.Extensions.Logging.Abstractions.dll": { "assemblyVersion": "6.0.0.0", "fileVersion": "6.0.21.52210" } } }, "Microsoft.Extensions.Options/6.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", "Microsoft.Extensions.Primitives": "6.0.0" }, "runtime": { "lib/netstandard2.1/Microsoft.Extensions.Options.dll": { "assemblyVersion": "6.0.0.0", "fileVersion": "6.0.21.52210" } } }, "Microsoft.Extensions.Primitives/6.0.0": { "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" }, "runtime": { "lib/net6.0/Microsoft.Extensions.Primitives.dll": { "assemblyVersion": "6.0.0.0", "fileVersion": "6.0.21.52210" } } }, "Newtonsoft.Json/13.0.1": { "runtime": { "lib/netstandard2.0/Newtonsoft.Json.dll": { "assemblyVersion": "13.0.0.0", "fileVersion": "13.0.1.25517" } } }, "Serilog/2.10.0": { "runtime": { "lib/netstandard2.1/Serilog.dll": { "assemblyVersion": "2.0.0.0", "fileVersion": "2.10.0.0" } } }, "Serilog.Sinks.Console/4.0.1": { "dependencies": { "Serilog": "2.10.0" }, "runtime": { "lib/net5.0/Serilog.Sinks.Console.dll": { "assemblyVersion": "4.0.1.0", "fileVersion": "4.0.1.0" } } }, "System.Diagnostics.DiagnosticSource/6.0.0": { "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Runtime.CompilerServices.Unsafe/6.0.0": {}, "TestCommon/1.0.0": { "dependencies": { "AWSSDK.Core": "3.7.10.6" }, "runtime": { "TestCommon.dll": {} } } } }, "libraries": { "TestLibrary/1.0.0": { "type": "project", "serviceable": false, "sha512": "" }, "AWSSDK.Core/3.7.10.6": { "type": "package", "serviceable": true, "sha512": "sha512-kHBB+QmosVaG6DpngXQ8OlLVVNMzltNITfsRr68Z90qO7dSqJ2EHNd8dtBU1u3AQQLqqFHOY0lfmbpexeH6Pew==", "path": "awssdk.core/3.7.10.6", "hashPath": "awssdk.core.3.7.10.6.nupkg.sha512" } } } ================================================ FILE: test/integration/testdata/image-debian-match-coverage/golang/go.mod ================================================ module github.com/anchore/coverage go 1.18 require github.com/google/uuid v1.3.0 ================================================ FILE: test/integration/testdata/image-debian-match-coverage/golang/go.sum ================================================ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= ================================================ FILE: test/integration/testdata/image-debian-match-coverage/golang/main.go ================================================ package main import ( "fmt" "github.com/google/uuid" ) func main() { fmt.Println(uuid.New()) } ================================================ FILE: test/integration/testdata/image-debian-match-coverage/haskell/cabal.project.freeze ================================================ active-repositories: hackage.haskell.org:merge constraints: any.Cabal ==3.2.1.0, any.Diff ==0.4.1, any.HTTP ==4000.3.16, any.HUnit ==1.6.2.0, any.OneTuple ==0.3.1, tls +compat -hans +network, any.Only ==0.1, any.PyF ==0.10.2.0, any.QuickCheck ==2.14.2, semigroups +binary +bytestring -bytestring-builder +containers +deepseq +hashable +tagged +template-haskell +text +transformers +unordered-containers, any.RSA ==2.4.1, any.SHA ==1.6.4.4, void -safe, any.Spock ==0.14.0.0, index-state: hackage.haskell.org 2022-07-07T01:01:53Z ================================================ FILE: test/integration/testdata/image-debian-match-coverage/haskell/stack.yaml ================================================ flags: {} extra-package-dbs: [] packages: - . resolver: lts-18.28 extra-deps: - ShellCheck-0.8.0@sha256:353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df,3297 - colourista-0.1.0.1@sha256:98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e,3307 - language-docker-11.0.0@sha256:3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55,3883 - spdx-1.0.0.2@sha256:7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed,2008 - hspec-2.9.4@sha256:658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3,1709 - hspec-core-2.9.4@sha256:a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8,6498 - hspec-discover-2.9.4@sha256:fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6,2165 - stm-2.5.0.2@sha256:e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55,2314 ghc-options: "$everything": -haddock ================================================ FILE: test/integration/testdata/image-debian-match-coverage/java/generate-fixtures.md ================================================ See the syft/cataloger/java/testdata/java-builds dir to generate test fixtures and copy to here manually. ================================================ FILE: test/integration/testdata/image-debian-match-coverage/javascript/pkg-json/package.json ================================================ { "version": "6.14.6", "name": "npm", "description": "a package manager for JavaScript", "keywords": [ "install", "modules", "package manager", "package.json" ], "preferGlobal": true, "config": { "publishtest": false }, "homepage": "https://docs.npmjs.com/", "author": "Isaac Z. Schlueter (http://blog.izs.me)", "repository": { "type": "git", "url": "https://github.com/npm/cli" }, "bugs": { "url": "https://npm.community/c/bugs" }, "directories": { "bin": "./bin", "doc": "./doc", "lib": "./lib", "man": "./man" }, "main": "./lib/npm.js", "bin": { "npm": "./bin/npm-cli.js", "npx": "./bin/npx-cli.js" }, "dependencies": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", "ansicolors": "~0.3.2", "write-file-atomic": "^2.4.3" }, "bundleDependencies": [ "abbrev", "ansicolors", "ansistyles", "write-file-atomic" ], "devDependencies": { "deep-equal": "^1.0.1", "get-stream": "^4.1.0", "licensee": "^7.0.3", "marked": "^0.6.3", "marked-man": "^0.6.0", "npm-registry-couchapp": "^2.7.4", "npm-registry-mock": "^1.3.1", "require-inject": "^1.4.4", "sprintf-js": "^1.1.2", "standard": "^11.0.1", "tacks": "^1.3.0", "tap": "^12.7.0", "tar-stream": "^2.1.0" }, "scripts": { "dumpconf": "env | grep npm | sort | uniq", "prepare": "node bin/npm-cli.js rebuild && node bin/npm-cli.js --no-audit --no-timing prune --prefix=. --no-global && rimraf test/*/*/node_modules && make -j4 mandocs", "preversion": "bash scripts/update-authors.sh && git add AUTHORS && git commit -m \"update AUTHORS\" || true", "licenses": "licensee --production --errors-only", "tap": "tap -J --timeout 300 --no-esm", "tap-cover": "tap -J --nyc-arg=--cache --coverage --timeout 600 --no-esm", "lint": "standard", "pretest": "npm run lint", "test": "npm run test-tap --", "test:nocleanup": "NO_TEST_CLEANUP=1 npm run test --", "sudotest": "sudo npm run tap -- \"test/tap/*.js\"", "sudotest:nocleanup": "sudo NO_TEST_CLEANUP=1 npm run tap -- \"test/tap/*.js\"", "posttest": "rimraf test/npm_cache*", "test-coverage": "npm run tap-cover -- \"test/tap/*.js\" \"test/network/*.js\"", "test-tap": "npm run tap -- \"test/tap/*.js\" \"test/network/*.js\"", "test-node": "tap --timeout 240 \"test/tap/*.js\" \"test/network/*.js\"" }, "license": "Artistic-2.0", "engines": { "node": "6 >=6.2.0 || 8 || >=9.3.0" } } ================================================ FILE: test/integration/testdata/image-debian-match-coverage/python/dist-info/METADATA ================================================ Metadata-Version: 2.1 Name: Pygments Version: 2.6.1 Summary: Pygments is a syntax highlighting package written in Python. Home-page: https://pygments.org/ Author: Georg Brandl Author-email: georg@python.org License: BSD License Keywords: syntax highlighting Platform: any Classifier: License :: OSI Approved :: BSD License Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators Classifier: Development Status :: 6 - Mature Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: OS Independent Classifier: Topic :: Text Processing :: Filters Classifier: Topic :: Utilities Requires-Python: >=3.5 Pygments ~~~~~~~~ Pygments is a syntax highlighting package written in Python. It is a generic syntax highlighter suitable for use in code hosting, forums, wikis or other applications that need to prettify source code. Highlights are: * a wide range of over 500 languages and other text formats is supported * special attention is paid to details, increasing quality by a fair amount * support for new languages and formats are added easily * a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences * it is usable as a command-line tool and as a library :copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS. :license: BSD, see LICENSE for details. ================================================ FILE: test/integration/testdata/image-debian-match-coverage/python/dist-info/top_level.txt ================================================ pygments ================================================ FILE: test/integration/testdata/image-debian-match-coverage/ruby/specifications/bundler.gemspec ================================================ # frozen_string_literal: true # -*- encoding: utf-8 -*- # stub: bundler 2.1.4 ruby lib Gem::Specification.new do |s| s.name = "bundler".freeze s.version = "2.1.4" s.required_rubygems_version = Gem::Requirement.new(">= 2.5.2".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/bundler/bundler/issues", "changelog_uri" => "https://github.com/bundler/bundler/blob/master/CHANGELOG.md", "homepage_uri" => "https://bundler.io/", "source_code_uri" => "https://github.com/bundler/bundler/" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze, "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".freeze, "Stephanie Morillo".freeze, "Chris Morris".freeze, "James Wen".freeze, "Tim Moore".freeze, "Andr\u00E9 Medeiros".freeze, "Jessica Lynn Suttles".freeze, "Terence Lee".freeze, "Carl Lerche".freeze, "Yehuda Katz".freeze] s.bindir = "exe".freeze s.date = "2020-01-05" s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably".freeze s.email = ["team@bundler.io".freeze] s.executables = ["bundle".freeze, "bundler".freeze] s.files = ["exe/bundle".freeze, "exe/bundler".freeze] s.homepage = "https://bundler.io".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze) s.rubygems_version = "3.1.2".freeze s.summary = "The best way to manage your application's dependencies".freeze s.installed_by_version = "3.1.2" if s.respond_to? :installed_by_version end ================================================ FILE: test/integration/testdata/image-debian-match-coverage/usr/lib/os-release ================================================ PRETTY_NAME="Debian GNU/Linux 8 (jessie)" NAME="Debian GNU/Linux" VERSION_ID="8" VERSION="8 (jessie)" ID=debian HOME_URL="http://www.debian.org/" SUPPORT_URL="http://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ================================================ FILE: test/integration/testdata/image-debian-match-coverage/var/lib/dpkg/status ================================================ Package: apt Status: install ok installed Priority: required Section: admin Installed-Size: 4064 Maintainer: APT Development Team Architecture: amd64 Version: 1.8.2 Source: apt-dev Replaces: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~) Provides: apt-transport-https (= 1.8.2) Depends: adduser, gpgv | gpgv2 | gpgv1, debian-archive-keyring, libapt-pkg5.0 (>= 1.7.0~alpha3~), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libgnutls30 (>= 3.6.6), libseccomp2 (>= 1.0.1), libstdc++6 (>= 5.2) Recommends: ca-certificates Suggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base Breaks: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~), aptitude (<< 0.8.10) Conffiles: /etc/apt/apt.conf.d/01autoremove 76120d358bc9037bb6358e737b3050b5 /etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3 /etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a /etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3 Description: commandline package manager This package provides commandline tools for searching and managing as well as querying information about packages as a low-level access to all features of the libapt-pkg library. . These include: * apt-get for retrieval of packages and information about them from authenticated sources and for installation, upgrade and removal of packages together with their dependencies * apt-cache for querying available information about installed as well as installable packages * apt-cdrom to use removable media as a source for packages * apt-config as an interface to the configuration settings * apt-key as an interface to manage authentication keys ================================================ FILE: test/integration/testdata/image-jvm-match-coverage/Dockerfile ================================================ FROM scratch COPY . . ================================================ FILE: test/integration/testdata/image-jvm-match-coverage/opt/java/openjdk/release ================================================ JAVA_VERSION="1.8.0_400" FULL_VERSION="1.8.0_400-b07" NOPE_SEMANTIC_VERSION="8.0.400+7" IMPLEMENTOR="Oracle" IMAGE_TYPE="JDK" ================================================ FILE: test/integration/testdata/image-portage-match-coverage/Dockerfile ================================================ FROM scratch COPY . . ================================================ FILE: test/integration/testdata/image-portage-match-coverage/etc/os-release ================================================ NAME=Gentoo ID=gentoo PRETTY_NAME="Gentoo Linux" ANSI_COLOR="1;32" HOME_URL="https://www.gentoo.org/" SUPPORT_URL="https://www.gentoo.org/support/" BUG_REPORT_URL="https://bugs.gentoo.org/" VERSION_ID="2.8" ================================================ FILE: test/integration/testdata/image-portage-match-coverage/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS ================================================ dir /usr dir /usr/bin obj /usr/bin/skopeo 376c02bd3b22804df8fdfdc895e7dbfb 1649284374 dir /etc dir /etc/containers obj /etc/containers/policy.json c01eb6950f03419e09d4fc88cb42ff6f 1649284375 dir /etc/containers/registries.d obj /etc/containers/registries.d/default.yaml e6e66cd3c24623e0667f26542e0e08f6 1649284375 dir /var dir /var/lib dir /var/lib/atomic dir /var/lib/atomic/sigstore obj /var/lib/atomic/sigstore/.keep_app-containers_skopeo-0 d41d8cd98f00b204e9800998ecf8427e 1649284375 ================================================ FILE: test/integration/testdata/image-portage-match-coverage/var/db/pkg/app-containers/skopeo-1.5.1/LICENSE ================================================ Apache-2.0 BSD BSD-2 CC-BY-SA-4.0 ISC MIT ================================================ FILE: test/integration/testdata/image-portage-match-coverage/var/db/pkg/app-containers/skopeo-1.5.1/SIZE ================================================ 27937835 ================================================ FILE: test/integration/testdata/image-portage-match-coverage/var/db/repos/gentoo/skel.ebuild ================================================ # Copyright 1999-2022 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # NOTE: The comments in this file are for instruction and documentation. # They're not meant to appear with your final, production ebuild. Please # remember to remove them before submitting or committing your ebuild. That # doesn't mean you can't add your own comments though. # The EAPI variable tells the ebuild format in use. # It is suggested that you use the latest EAPI approved by the Council. # The PMS contains specifications for all EAPIs. Eclasses will test for this # variable if they need to use features that are not universal in all EAPIs. # If an eclass doesn't support latest EAPI, use the previous EAPI instead. EAPI=7 # inherit lists eclasses to inherit functions from. For example, an ebuild # that needs the eautoreconf function from autotools.eclass won't work # without the following line: #inherit autotools # # Eclasses tend to list descriptions of how to use their functions properly. # Take a look at the eclass/ directory for more examples. # Short one-line description of this package. DESCRIPTION="This is a sample skeleton ebuild file" ================================================ FILE: test/integration/testdata/image-rust-auditable-match-coverage/Dockerfile ================================================ # An image containing the example hello-auditable binary from https://github.com/Shnatsel/rust-audit/tree/master/hello-auditable FROM docker.io/tofay/hello-rust-auditable:latest ================================================ FILE: test/integration/testdata/image-sles-match-coverage/Dockerfile ================================================ FROM scratch COPY . . ================================================ FILE: test/integration/testdata/image-sles-match-coverage/etc/os-release ================================================ NAME="SLES" VERSION="12-SP5" VERSION_ID="12.5" PRETTY_NAME="SUSE Linux Enterprise Server 12 SP5" ID="sles" ID_LIKE="suse" ANSI_COLOR="0;32" CPE_NAME="cpe:/o:suse:sles:12:sp5" DOCUMENTATION_URL="https://documentation.suse.com/" ================================================ FILE: test/integration/testdata/image-sles-match-coverage/var/lib/rpm/generate-fixture.sh ================================================ #!/usr/bin/env bash set -eux docker create --name generate-rpmdb-fixture sles12sp5:latest sh -c 'tail -f /dev/null' function cleanup { docker kill generate-rpmdb-fixture docker rm generate-rpmdb-fixture } trap cleanup EXIT docker start generate-rpmdb-fixture docker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF mkdir -p /scratch cd /scratch rpm --initdb --dbpath /scratch curl -sSLO https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm rpm --dbpath /scratch -ivh dive_0.9.2_linux_amd64.rpm rm dive_0.9.2_linux_amd64.rpm rpm --dbpath /scratch -qa EOF docker cp generate-rpmdb-fixture:/scratch/Packages . ================================================ FILE: test/integration/testdata/sbom/syft-sbom-with-kb-packages.json ================================================ { "artifacts": [ { "id": "eeb36c1c-c03a-425b-901f-df918cc3757e", "name": "10816", "version": "3200970", "type": "msrc-kb" } ], "artifactRelationships": [], "source": { "type": "image", "target": { "userInput": "my-awesome-image:latest", "scope": "Squashed" } }, "distro": { "name": "windows", "version": "10816", "idLike": "" }, "descriptor": { "name": "syft", "version": "v0.19.1" }, "schema": { "version": "1.1.0", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } ================================================ FILE: test/integration/testdata/sbom/syft-sbom-with-unknown-packages.json ================================================ { "artifacts": [ { "id": "eeb36c1c-c03a-425b-901f-df918cc3757e", "name": "my-package", "version": "1.0.5", "type": "idris", "language": "idris", "cpes": [ "cpe:2.3:a:my-package:my-package:1.0.5:*:*:*:*:*:*:*", "cpe:2.3:a:bogus:my-package:1.0.5:*:*:*:*:*:*:*" ] } ], "artifactRelationships": [], "source": { "type": "image", "target": { "userInput": "my-awesome-image:latest", "scope": "Squashed" } }, "distro": { "name": "ubuntu", "version": "20.04", "idLike": "" }, "descriptor": { "name": "syft", "version": "v0.19.1" }, "schema": { "version": "1.1.0", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" } } ================================================ FILE: test/integration/testdata/skopeo-policy.json ================================================ { "default": [ { "type": "insecureAcceptAnything" } ], "transports": { "docker-daemon": { "": [ { "type": "insecureAcceptAnything" } ] } } } ================================================ FILE: test/integration/testdata/snapshot/TestDatabaseDiff.golden ================================================ [ { "reason": "added", "id": "ALAS-2022-145", "namespace": "amazon:distro:amazonlinux:2022", "packages": [ "curl", "curl-debuginfo", "curl-debugsource", "curl-minimal", "curl-minimal-debuginfo", "libcurl", "libcurl-debuginfo", "libcurl-devel", "libcurl-minimal", "libcurl-minimal-debuginfo" ] }, { "reason": "added", "id": "ALAS-2022-146", "namespace": "amazon:distro:amazonlinux:2022", "packages": [ "lua", "lua-debuginfo", "lua-debugsource", "lua-devel", "lua-libs", "lua-libs-debuginfo", "lua-static" ] }, { "reason": "added", "id": "ALAS-2022-147", "namespace": "amazon:distro:amazonlinux:2022", "packages": [ "openssl", "openssl-debuginfo", "openssl-debugsource", "openssl-devel", "openssl-libs", "openssl-libs-debuginfo", "openssl-perl" ] }, { "reason": "added", "id": "ALAS-2022-148", "namespace": "amazon:distro:amazonlinux:2022", "packages": [ "rsync", "rsync-daemon", "rsync-debuginfo", "rsync-debugsource" ] }, { "reason": "added", "id": "ALAS-2022-149", "namespace": "amazon:distro:amazonlinux:2022", "packages": [ "python3-subversion", "python3-subversion-debuginfo", "subversion", "subversion-debuginfo", "subversion-debugsource", "subversion-devel", "subversion-devel-debuginfo", "subversion-javahl", "subversion-libs", "subversion-libs-debuginfo", "subversion-perl", "subversion-perl-debuginfo", "subversion-tools", "subversion-tools-debuginfo" ] }, { "reason": "added", "id": "ALAS-2022-150", "namespace": "amazon:distro:amazonlinux:2022", "packages": [ "bpftool", "bpftool-debuginfo", "kernel", "kernel-debuginfo", "kernel-debuginfo-common-x86_64", "kernel-devel", "kernel-headers", "kernel-libbpf", "kernel-libbpf-devel", "kernel-libbpf-static", "kernel-livepatch-5.15.72-43.134", "kernel-tools", "kernel-tools-debuginfo", "kernel-tools-devel", "perf", "perf-debuginfo", "python3-perf", "python3-perf-debuginfo" ] }, { "reason": "added", "id": "ALASDOCKER-2022-020", "namespace": "amazon:distro:amazonlinux:2", "packages": [ "runc", "runc-debuginfo" ] }, { "reason": "added", "id": "ALASDOCKER-2022-021", "namespace": "amazon:distro:amazonlinux:2", "packages": [ "containerd", "containerd-debuginfo", "containerd-stress", "docker", "docker-debuginfo" ] }, { "reason": "added", "id": "ALASKERNEL-5.10-2022-020", "namespace": "amazon:distro:amazonlinux:2", "packages": [ "bpftool", "bpftool-debuginfo", "kernel", "kernel-debuginfo", "kernel-debuginfo-common-x86_64", "kernel-devel", "kernel-headers", "kernel-livepatch-5.10.144-127.601", "kernel-tools", "kernel-tools-debuginfo", "kernel-tools-devel", "perf", "perf-debuginfo", "python-perf", "python-perf-debuginfo" ] }, { "reason": "added", "id": "ALASKERNEL-5.15-2022-008", "namespace": "amazon:distro:amazonlinux:2", "packages": [ "bpftool", "bpftool-debuginfo", "kernel", "kernel-debuginfo", "kernel-debuginfo-common-x86_64", "kernel-devel", "kernel-headers", "kernel-livepatch-5.15.69-37.134", "kernel-tools", "kernel-tools-debuginfo", "kernel-tools-devel", "perf", "perf-debuginfo", "python-perf", "python-perf-debuginfo" ] }, { "reason": "added", "id": "ALASKERNEL-5.4-2022-036", "namespace": "amazon:distro:amazonlinux:2", "packages": [ "bpftool", "bpftool-debuginfo", "kernel", "kernel-debuginfo", "kernel-debuginfo-common-x86_64", "kernel-devel", "kernel-headers", "kernel-tools", "kernel-tools-debuginfo", "kernel-tools-devel", "perf", "perf-debuginfo", "python-perf", "python-perf-debuginfo" ] }, { "reason": "changed", "id": "CVE-2013-0350", "namespace": "debian:distro:debian:12", "packages": [ "pktstat" ] }, { "reason": "added", "id": "CVE-2014-125002", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125003", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125004", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125005", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125006", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125007", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125008", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125009", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125010", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125011", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125012", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125013", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125014", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125015", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125016", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125017", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125018", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125019", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125020", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125021", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125022", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125023", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125024", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-125025", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2014-6274", "namespace": "debian:distro:debian:12", "packages": [ "git-annex" ] }, { "reason": "changed", "id": "CVE-2015-0283", "namespace": "redhat:distro:redhat:7", "packages": [ "ipa", "slapi-nis" ] }, { "reason": "changed", "id": "CVE-2015-1827", "namespace": "redhat:distro:redhat:7", "packages": [ "ipa", "slapi-nis" ] }, { "reason": "changed", "id": "CVE-2015-2779", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [ "quassel" ] }, { "reason": "added", "id": "CVE-2017-12976", "namespace": "debian:distro:debian:12", "packages": [ "git-annex" ] }, { "reason": "added", "id": "CVE-2017-20149", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2018-10857", "namespace": "debian:distro:debian:12", "packages": [ "git-annex" ] }, { "reason": "added", "id": "CVE-2018-10859", "namespace": "debian:distro:debian:12", "packages": [ "git-annex" ] }, { "reason": "changed", "id": "CVE-2018-16860", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "heimdal", "samba" ] }, { "reason": "changed", "id": "CVE-2018-17954", "namespace": "nvd:cpe", "packages": [ "openstack_cloud", "openstack_cloud_crowbar" ] }, { "reason": "added", "id": "CVE-2018-18446", "namespace": "nvd:cpe", "packages": [ "paint.net" ] }, { "reason": "added", "id": "CVE-2018-18447", "namespace": "nvd:cpe", "packages": [ "paint.net" ] }, { "reason": "changed", "id": "CVE-2019-12098", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "heimdal" ] }, { "reason": "changed", "id": "CVE-2019-13699", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13700", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13701", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13702", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13703", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13704", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13706", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13708", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13709", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13710", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13714", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13715", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13716", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13717", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13718", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-13719", "namespace": "nvd:cpe", "packages": [ "backports_sle", "chrome" ] }, { "reason": "changed", "id": "CVE-2019-18906", "namespace": "nvd:cpe", "packages": [ "cryptctl" ] }, { "reason": "removed", "id": "CVE-2019-19274", "namespace": "debian:distro:debian:12", "packages": [ "python3-typed-ast" ] }, { "reason": "removed", "id": "CVE-2019-19275", "namespace": "debian:distro:debian:12", "packages": [ "python3-typed-ast" ] }, { "reason": "changed", "id": "CVE-2019-20326", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "gthumb" ] }, { "reason": "changed", "id": "CVE-2019-5477", "namespace": "nvd:cpe", "packages": [ "nokogiri" ] }, { "reason": "changed", "id": "CVE-2019-5888", "namespace": "nvd:cpe", "packages": [ "geocall" ] }, { "reason": "changed", "id": "CVE-2019-5889", "namespace": "nvd:cpe", "packages": [ "geocall" ] }, { "reason": "changed", "id": "CVE-2019-5890", "namespace": "nvd:cpe", "packages": [ "geocall" ] }, { "reason": "changed", "id": "CVE-2019-5891", "namespace": "nvd:cpe", "packages": [ "geocall" ] }, { "reason": "changed", "id": "CVE-2019-5924", "namespace": "nvd:cpe", "packages": [ "smart_forms" ] }, { "reason": "changed", "id": "CVE-2019-6002", "namespace": "nvd:cpe", "packages": [ "central_dogma" ] }, { "reason": "changed", "id": "CVE-2019-6166", "namespace": "nvd:cpe", "packages": [ "service_bridge" ] }, { "reason": "changed", "id": "CVE-2019-6167", "namespace": "nvd:cpe", "packages": [ "service_bridge" ] }, { "reason": "changed", "id": "CVE-2019-6168", "namespace": "nvd:cpe", "packages": [ "service_bridge" ] }, { "reason": "changed", "id": "CVE-2019-6169", "namespace": "nvd:cpe", "packages": [ "service_bridge" ] }, { "reason": "changed", "id": "CVE-2019-6177", "namespace": "nvd:cpe", "packages": [ "solution_center" ] }, { "reason": "changed", "id": "CVE-2019-6178", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-6179", "namespace": "nvd:cpe", "packages": [ "xclarity_administrator", "xclarity_integrator" ] }, { "reason": "changed", "id": "CVE-2019-6180", "namespace": "nvd:cpe", "packages": [ "xclarity_administrator" ] }, { "reason": "changed", "id": "CVE-2019-6181", "namespace": "nvd:cpe", "packages": [ "xclarity_administrator" ] }, { "reason": "changed", "id": "CVE-2019-6182", "namespace": "nvd:cpe", "packages": [ "xclarity_administrator" ] }, { "reason": "changed", "id": "CVE-2019-6727", "namespace": "nvd:cpe", "packages": [ "phantompdf", "reader" ] }, { "reason": "changed", "id": "CVE-2019-6728", "namespace": "nvd:cpe", "packages": [ "phantompdf", "reader" ] }, { "reason": "changed", "id": "CVE-2019-6730", "namespace": "nvd:cpe", "packages": [ "phantompdf", "reader" ] }, { "reason": "changed", "id": "CVE-2019-6733", "namespace": "nvd:cpe", "packages": [ "phantompdf", "reader" ] }, { "reason": "changed", "id": "CVE-2019-6734", "namespace": "nvd:cpe", "packages": [ "phantompdf", "reader" ] }, { "reason": "changed", "id": "CVE-2019-6735", "namespace": "nvd:cpe", "packages": [ "phantompdf", "reader" ] }, { "reason": "changed", "id": "CVE-2019-6737", "namespace": "nvd:cpe", "packages": [ "safepay" ] }, { "reason": "changed", "id": "CVE-2019-6741", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-6743", "namespace": "nvd:cpe", "packages": [ "mi6_browser" ] }, { "reason": "changed", "id": "CVE-2019-6746", "namespace": "nvd:cpe", "packages": [ "foxit_studio_photo" ] }, { "reason": "changed", "id": "CVE-2019-6747", "namespace": "nvd:cpe", "packages": [ "foxit_studio_photo" ] }, { "reason": "changed", "id": "CVE-2019-6748", "namespace": "nvd:cpe", "packages": [ "foxit_studio_photo" ] }, { "reason": "changed", "id": "CVE-2019-6749", "namespace": "nvd:cpe", "packages": [ "foxit_studio_photo" ] }, { "reason": "changed", "id": "CVE-2019-6750", "namespace": "nvd:cpe", "packages": [ "foxit_studio_photo" ] }, { "reason": "changed", "id": "CVE-2019-6751", "namespace": "nvd:cpe", "packages": [ "foxit_studio_photo" ] }, { "reason": "changed", "id": "CVE-2019-6753", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6754", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6755", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6756", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6757", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6758", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6759", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6760", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6761", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6762", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6763", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6764", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6765", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6766", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6767", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6768", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6769", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6770", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6771", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6772", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6773", "namespace": "nvd:cpe", "packages": [ "foxit_reader", "phantompdf" ] }, { "reason": "changed", "id": "CVE-2019-6812", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-6822", "namespace": "nvd:cpe", "packages": [ "zelio_soft_2" ] }, { "reason": "changed", "id": "CVE-2019-6823", "namespace": "nvd:cpe", "packages": [ "proclima" ] }, { "reason": "changed", "id": "CVE-2019-6824", "namespace": "nvd:cpe", "packages": [ "proclima" ] }, { "reason": "changed", "id": "CVE-2019-6827", "namespace": "nvd:cpe", "packages": [ "interactive_graphical_scada_system" ] }, { "reason": "changed", "id": "CVE-2019-7061", "namespace": "nvd:cpe", "packages": [ "acrobat_dc", "acrobat_reader_dc" ] }, { "reason": "changed", "id": "CVE-2019-7088", "namespace": "nvd:cpe", "packages": [ "acrobat_dc", "acrobat_reader_dc" ] }, { "reason": "changed", "id": "CVE-2019-7096", "namespace": "nvd:cpe", "packages": [ "flash_player", "flash_player_desktop_runtime" ] }, { "reason": "changed", "id": "CVE-2019-7107", "namespace": "nvd:cpe", "packages": [ "indesign" ] }, { "reason": "changed", "id": "CVE-2019-7108", "namespace": "nvd:cpe", "packages": [ "flash_player", "flash_player_desktop_runtime" ] }, { "reason": "changed", "id": "CVE-2019-7255", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7256", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7257", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7258", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7259", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7261", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7262", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7265", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7266", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7267", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7268", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7269", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7270", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-7273", "namespace": "nvd:cpe", "packages": [ "enterprise", "proton" ] }, { "reason": "changed", "id": "CVE-2019-7274", "namespace": "nvd:cpe", "packages": [ "enterprise", "proton" ] }, { "reason": "changed", "id": "CVE-2019-7275", "namespace": "nvd:cpe", "packages": [ "enterprise", "proton" ] }, { "reason": "changed", "id": "CVE-2019-7654", "namespace": "nvd:cpe", "packages": [ "streaming_engine" ] }, { "reason": "changed", "id": "CVE-2019-7655", "namespace": "nvd:cpe", "packages": [ "streaming_engine" ] }, { "reason": "changed", "id": "CVE-2019-7672", "namespace": "nvd:cpe", "packages": [ "flexair" ] }, { "reason": "changed", "id": "CVE-2019-8292", "namespace": "nvd:cpe", "packages": [ "online_store_system" ] }, { "reason": "changed", "id": "CVE-2019-8625", "namespace": "nvd:cpe", "packages": [ "icloud", "itunes", "webkitgtk+" ] }, { "reason": "changed", "id": "CVE-2019-8674", "namespace": "nvd:cpe", "packages": [ "safari", "webkitgtk" ] }, { "reason": "changed", "id": "CVE-2019-8719", "namespace": "nvd:cpe", "packages": [ "icloud", "itunes", "webkitgtk+" ] }, { "reason": "added", "id": "CVE-2019-8764", "namespace": "nvd:cpe", "packages": [ "webkitgtk+" ] }, { "reason": "changed", "id": "CVE-2019-8813", "namespace": "nvd:cpe", "packages": [ "icloud", "itunes", "safari", "webkitgtk+" ] }, { "reason": "changed", "id": "CVE-2019-8987", "namespace": "nvd:cpe", "packages": [ "data_science_for_aws", "spotfire_data_science" ] }, { "reason": "changed", "id": "CVE-2019-8988", "namespace": "nvd:cpe", "packages": [ "data_science_for_aws", "spotfire_data_science" ] }, { "reason": "changed", "id": "CVE-2019-8990", "namespace": "nvd:cpe", "packages": [ "activematrix_businessworks" ] }, { "reason": "changed", "id": "CVE-2019-8991", "namespace": "nvd:cpe", "packages": [ "activematrix_bpm", "activematrix_policy_director", "activematrix_service_bus", "activematrix_service_grid", "silver_fabric_enabler" ] }, { "reason": "changed", "id": "CVE-2019-8992", "namespace": "nvd:cpe", "packages": [ "activematrix_bpm", "activematrix_policy_director", "activematrix_service_bus", "activematrix_service_grid", "silver_fabric_enabler" ] }, { "reason": "changed", "id": "CVE-2019-8993", "namespace": "nvd:cpe", "packages": [ "activematrix_bpm", "activematrix_policy_director", "activematrix_service_bus", "activematrix_service_grid", "silver_fabric_enabler" ] }, { "reason": "changed", "id": "CVE-2019-8995", "namespace": "nvd:cpe", "packages": [ "activematrix_bpm", "silver_fabric_enabler" ] }, { "reason": "changed", "id": "CVE-2019-9213", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-9445", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2019-9850", "namespace": "nvd:cpe", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2019-9851", "namespace": "nvd:cpe", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2019-9855", "namespace": "nvd:cpe", "packages": [ "libreoffice" ] }, { "reason": "added", "id": "CVE-2020-0093", "namespace": "nvd:cpe", "packages": [ "libexif" ] }, { "reason": "added", "id": "CVE-2020-0181", "namespace": "nvd:cpe", "packages": [ "libexif" ] }, { "reason": "added", "id": "CVE-2020-0198", "namespace": "nvd:cpe", "packages": [ "libexif" ] }, { "reason": "changed", "id": "CVE-2020-10735", "namespace": "nvd:cpe", "packages": [ "python", "quay", "software_collections" ] }, { "reason": "changed", "id": "CVE-2020-10735", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "changed", "id": "CVE-2020-10735", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "changed", "id": "CVE-2020-10735", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "changed", "id": "CVE-2020-10735", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "changed", "id": "CVE-2020-10735", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "removed", "id": "CVE-2020-12802", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2020-14129", "namespace": "nvd:cpe", "packages": [ "xiaomi" ] }, { "reason": "changed", "id": "CVE-2020-14131", "namespace": "nvd:cpe", "packages": [ "xiaomi" ] }, { "reason": "added", "id": "CVE-2020-14305", "namespace": "nvd:cpe", "packages": [ "cloud_backup" ] }, { "reason": "added", "id": "CVE-2020-14314", "namespace": "nvd:cpe", "packages": [ "starwind_virtual_san" ] }, { "reason": "changed", "id": "CVE-2020-14409", "namespace": "nvd:cpe", "packages": [ "simple_directmedia_layer", "starwind_virtual_san" ] }, { "reason": "changed", "id": "CVE-2020-16593", "namespace": "nvd:cpe", "packages": [ "binutils", "cloud_backup", "ontap_select_deploy_administration_utility", "solidfire_&_hci_management_node" ] }, { "reason": "changed", "id": "CVE-2020-17531", "namespace": "nvd:cpe", "packages": [ "tapestry" ] }, { "reason": "added", "id": "CVE-2020-24394", "namespace": "nvd:cpe", "packages": [ "sd-wan_edge", "starwind_virtual_san" ] }, { "reason": "added", "id": "CVE-2020-25656", "namespace": "nvd:cpe", "packages": [ "starwind_virtual_san" ] }, { "reason": "changed", "id": "CVE-2020-25692", "namespace": "nvd:cpe", "packages": [ "cloud_backup", "openldap" ] }, { "reason": "changed", "id": "CVE-2020-26247", "namespace": "nvd:cpe", "packages": [ "nokogiri" ] }, { "reason": "added", "id": "CVE-2020-26839", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26840", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26841", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26842", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26843", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26844", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26845", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26846", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26847", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26848", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26849", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26850", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26851", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26852", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26853", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26854", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26855", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26856", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26857", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26858", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26859", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26860", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26861", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26862", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26863", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26864", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26865", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2020-26866", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2020-2778", "namespace": "nvd:cpe", "packages": [ "7-mode_transition_tool", "active_iq_unified_manager", "cloud_backup", "cloud_secure_agent", "e-series_performance_analyzer", "e-series_santricity_os_controller", "e-series_santricity_web_services", "jdk", "jre", "oncommand_insight", "oncommand_workflow_automation", "openjdk", "plug-in_for_symantec_netbackup", "santricity_unified_manager", "snapmanager", "steelstore_cloud_integrated_storage", "storagegrid" ] }, { "reason": "changed", "id": "CVE-2020-2783", "namespace": "nvd:cpe", "packages": [ "outside_in_technology" ] }, { "reason": "changed", "id": "CVE-2020-2785", "namespace": "nvd:cpe", "packages": [ "outside_in_technology" ] }, { "reason": "changed", "id": "CVE-2020-2786", "namespace": "nvd:cpe", "packages": [ "outside_in_technology" ] }, { "reason": "changed", "id": "CVE-2020-2787", "namespace": "nvd:cpe", "packages": [ "outside_in_technology" ] }, { "reason": "changed", "id": "CVE-2020-27918", "namespace": "nvd:cpe", "packages": [ "icloud", "itunes", "safari", "webkitgtk+" ] }, { "reason": "changed", "id": "CVE-2020-28383", "namespace": "nvd:cpe", "packages": [ "jt2go", "solid_edge", "teamcenter_visualization" ] }, { "reason": "changed", "id": "CVE-2020-28851", "namespace": "redhat:distro:redhat:8", "packages": [ "container-tools:1.0/buildah", "container-tools:1.0/podman", "container-tools:2.0/buildah", "container-tools:2.0/podman", "container-tools:rhel8/buildah", "container-tools:rhel8/podman", "git-lfs" ] }, { "reason": "changed", "id": "CVE-2020-28852", "namespace": "redhat:distro:redhat:8", "packages": [ "container-tools:1.0/buildah", "container-tools:1.0/podman", "container-tools:2.0/buildah", "container-tools:2.0/podman", "container-tools:rhel8/buildah", "container-tools:rhel8/podman", "git-lfs" ] }, { "reason": "changed", "id": "CVE-2020-29651", "namespace": "nvd:cpe", "packages": [ "py", "zfs_storage_appliance_kit" ] }, { "reason": "added", "id": "CVE-2020-35538", "namespace": "redhat:distro:redhat:8", "packages": [ "libjpeg-turbo" ] }, { "reason": "added", "id": "CVE-2020-35538", "namespace": "redhat:distro:redhat:9", "packages": [ "libjpeg-turbo" ] }, { "reason": "added", "id": "CVE-2020-36322", "namespace": "nvd:cpe", "packages": [ "starwind_virtual_san" ] }, { "reason": "added", "id": "CVE-2020-36427", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "gthumb" ] }, { "reason": "changed", "id": "CVE-2020-36427", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "gthumb" ] }, { "reason": "changed", "id": "CVE-2020-4301", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "changed", "id": "CVE-2020-6624", "namespace": "nvd:cpe", "packages": [ "jhead" ] }, { "reason": "changed", "id": "CVE-2020-6625", "namespace": "nvd:cpe", "packages": [ "jhead" ] }, { "reason": "changed", "id": "CVE-2020-7774", "namespace": "debian:distro:debian:10", "packages": [ "node-y18n" ] }, { "reason": "changed", "id": "CVE-2020-7774", "namespace": "debian:distro:debian:11", "packages": [ "node-y18n" ] }, { "reason": "changed", "id": "CVE-2020-7774", "namespace": "debian:distro:debian:12", "packages": [ "node-y18n" ] }, { "reason": "changed", "id": "CVE-2020-7774", "namespace": "debian:distro:debian:unstable", "packages": [ "node-y18n" ] }, { "reason": "changed", "id": "CVE-2020-7774", "namespace": "nvd:cpe", "packages": [ "graalvm", "sinec_infrastructure_network_services", "y18n" ] }, { "reason": "changed", "id": "CVE-2020-9876", "namespace": "nvd:cpe", "packages": [ "icloud", "itunes" ] }, { "reason": "changed", "id": "CVE-2021-0696", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2021-0699", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2021-0951", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2021-20030", "namespace": "nvd:cpe", "packages": [ "global_management_system" ] }, { "reason": "changed", "id": "CVE-2021-20468", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "changed", "id": "CVE-2021-20594", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2021-20597", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2021-20599", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2021-22543", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-alt", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2021-22685", "namespace": "nvd:cpe", "packages": [ "access_controller" ] }, { "reason": "added", "id": "CVE-2021-27406", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2021-27853", "namespace": "nvd:cpe", "packages": [ "ieee_802.2", "ios_xe", "p802.1q" ] }, { "reason": "changed", "id": "CVE-2021-27854", "namespace": "nvd:cpe", "packages": [ "ieee_802.2", "p802.1q" ] }, { "reason": "changed", "id": "CVE-2021-27861", "namespace": "nvd:cpe", "packages": [ "ieee_802.2", "p802.1q" ] }, { "reason": "changed", "id": "CVE-2021-27862", "namespace": "nvd:cpe", "packages": [ "ieee_802.2", "p802.1q" ] }, { "reason": "changed", "id": "CVE-2021-29823", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "changed", "id": "CVE-2021-30496", "namespace": "nvd:cpe", "packages": [ "telegram" ] }, { "reason": "added", "id": "CVE-2021-31439", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2021-31439", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "changed", "id": "CVE-2021-31997", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "changed", "id": "CVE-2021-31997", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "changed", "id": "CVE-2021-31997", "namespace": "debian:distro:debian:unstable", "packages": [] }, { "reason": "changed", "id": "CVE-2021-31997", "namespace": "nvd:cpe", "packages": [ "python-postorius" ] }, { "reason": "changed", "id": "CVE-2021-33655", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2021-35452", "namespace": "debian:distro:debian:unstable", "packages": [ "libde265" ] }, { "reason": "changed", "id": "CVE-2021-36201", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2021-36369", "namespace": "nvd:cpe", "packages": [ "dropbear_ssh" ] }, { "reason": "added", "id": "CVE-2021-36369", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2021-36369", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2021-36369", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2021-36369", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2021-36369", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2021-36408", "namespace": "debian:distro:debian:unstable", "packages": [ "libde265" ] }, { "reason": "changed", "id": "CVE-2021-36409", "namespace": "debian:distro:debian:unstable", "packages": [ "libde265" ] }, { "reason": "changed", "id": "CVE-2021-36410", "namespace": "debian:distro:debian:unstable", "packages": [ "libde265" ] }, { "reason": "changed", "id": "CVE-2021-36411", "namespace": "debian:distro:debian:unstable", "packages": [ "libde265" ] }, { "reason": "changed", "id": "CVE-2021-3671", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "heimdal", "samba" ] }, { "reason": "changed", "id": "CVE-2021-3671", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "heimdal", "samba" ] }, { "reason": "changed", "id": "CVE-2021-36778", "namespace": "nvd:cpe", "packages": [ "rancher" ] }, { "reason": "changed", "id": "CVE-2021-36913", "namespace": "nvd:cpe", "packages": [ "redirection_for_contact_form_7" ] }, { "reason": "changed", "id": "CVE-2021-36915", "namespace": "nvd:cpe", "packages": [ "profile_builder" ] }, { "reason": "changed", "id": "CVE-2021-3807", "namespace": "nvd:cpe", "packages": [ "ansi-regex", "communications_cloud_native_core_policy" ] }, { "reason": "changed", "id": "CVE-2021-39009", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "changed", "id": "CVE-2021-39045", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "changed", "id": "CVE-2021-40017", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2021-40345", "namespace": "nvd:cpe", "packages": [ "nagios_xi" ] }, { "reason": "changed", "id": "CVE-2021-40394", "namespace": "nvd:cpe", "packages": [ "gerbv" ] }, { "reason": "changed", "id": "CVE-2021-4159", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-gke-5.4", "linux-gkeop-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2021-4159", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-kvm", "linux-oracle", "linux-raspi" ] }, { "reason": "added", "id": "CVE-2021-4217", "namespace": "alpine:distro:alpine:edge", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2021-4217", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2021-4217", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2021-4217", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2021-43466", "namespace": "nvd:cpe", "packages": [ "thymeleaf" ] }, { "reason": "changed", "id": "CVE-2021-43618", "namespace": "nvd:cpe", "packages": [ "gmp" ] }, { "reason": "changed", "id": "CVE-2021-43766", "namespace": "nvd:cpe", "packages": [ "odyssey", "postgresql" ] }, { "reason": "added", "id": "CVE-2021-43980", "namespace": "redhat:distro:redhat:8", "packages": [ "pki-deps:10.6/pki-servlet-engine" ] }, { "reason": "added", "id": "CVE-2021-43980", "namespace": "redhat:distro:redhat:9", "packages": [ "pki-servlet-engine" ] }, { "reason": "changed", "id": "CVE-2021-44171", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2021-46839", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2021-46840", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-0030", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-0194", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-0194", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-0529", "namespace": "alpine:distro:alpine:edge", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0529", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0529", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0529", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "unzip" ] }, { "reason": "added", "id": "CVE-2022-0530", "namespace": "alpine:distro:alpine:edge", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0530", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0530", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0530", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "unzip" ] }, { "reason": "changed", "id": "CVE-2022-0669", "namespace": "redhat:distro:redhat:7", "packages": [ "dpdk" ] }, { "reason": "changed", "id": "CVE-2022-0669", "namespace": "redhat:distro:redhat:8", "packages": [ "dpdk" ] }, { "reason": "changed", "id": "CVE-2022-0812", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-0836", "namespace": "nvd:cpe", "packages": [ "sema_api" ] }, { "reason": "changed", "id": "CVE-2022-1011", "namespace": "nvd:cpe", "packages": [ "build_of_quarkus", "codeready_linux_builder", "communications_cloud_native_core_binding_support_function", "developer_tools", "hci_baseboard_management_controller", "virtualization_host" ] }, { "reason": "changed", "id": "CVE-2022-1012", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-gke-5.4", "linux-gkeop-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-1097", "namespace": "redhat:distro:redhat:7", "packages": [ "firefox", "thunderbird" ] }, { "reason": "changed", "id": "CVE-2022-1097", "namespace": "redhat:distro:redhat:8", "packages": [ "firefox", "thunderbird" ] }, { "reason": "changed", "id": "CVE-2022-1253", "namespace": "debian:distro:debian:unstable", "packages": [ "libde265" ] }, { "reason": "changed", "id": "CVE-2022-1259", "namespace": "nvd:cpe", "packages": [ "build_of_quarkus", "integration_camel_k", "jboss_enterprise_application_platform", "openshift_application_runtimes", "single_sign-on", "undertow" ] }, { "reason": "changed", "id": "CVE-2022-1319", "namespace": "nvd:cpe", "packages": [ "openshift_application_runtimes", "single_sign-on", "undertow" ] }, { "reason": "changed", "id": "CVE-2022-1325", "namespace": "debian:distro:debian:unstable", "packages": [ "cimg" ] }, { "reason": "changed", "id": "CVE-2022-1354", "namespace": "nvd:cpe", "packages": [ "libtiff" ] }, { "reason": "changed", "id": "CVE-2022-1355", "namespace": "nvd:cpe", "packages": [ "libtiff" ] }, { "reason": "removed", "id": "CVE-2022-1480", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "chromium-browser" ] }, { "reason": "changed", "id": "CVE-2022-1560", "namespace": "nvd:cpe", "packages": [ "amministrazione_aperta" ] }, { "reason": "changed", "id": "CVE-2022-1882", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-20231", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20351", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20364", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20369", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-20369", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gke", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-20394", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-20397", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20409", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20409", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20409", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20409", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20410", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20412", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20413", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20415", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20416", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20417", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20418", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20419", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20420", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20421", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20421", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20421", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20421", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20421", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20422", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20422", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20422", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20422", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20422", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20423", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20423", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20423", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20423", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-20423", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20425", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20429", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20430", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20431", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20432", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20433", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20434", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20435", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20436", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20437", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20438", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20439", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20440", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-20464", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20830", "namespace": "nvd:cpe", "packages": [ "sd-wan_vmanage" ] }, { "reason": "changed", "id": "CVE-2022-20837", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20864", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20870", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20915", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20920", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-20944", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-21797", "namespace": "debian:distro:debian:12", "packages": [ "joblib" ] }, { "reason": "changed", "id": "CVE-2022-21936", "namespace": "nvd:cpe", "packages": [ "metasys_extended_application_and_data_server" ] }, { "reason": "added", "id": "CVE-2022-2249", "namespace": "nvd:cpe", "packages": [ "aura_communication_manager" ] }, { "reason": "changed", "id": "CVE-2022-22818", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-22818", "namespace": "nvd:cpe", "packages": [ "django" ] }, { "reason": "changed", "id": "CVE-2022-2308", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-23121", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23121", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23122", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23122", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23123", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23123", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23124", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23124", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23125", "namespace": "alpine:distro:alpine:3.16", "packages": [ "netatalk" ] }, { "reason": "added", "id": "CVE-2022-23125", "namespace": "alpine:distro:alpine:edge", "packages": [ "netatalk" ] }, { "reason": "changed", "id": "CVE-2022-2318", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-2318", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-2318", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-23833", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-23833", "namespace": "nvd:cpe", "packages": [ "django" ] }, { "reason": "changed", "id": "CVE-2022-24106", "namespace": "debian:distro:debian:10", "packages": [ "poppler" ] }, { "reason": "changed", "id": "CVE-2022-24106", "namespace": "debian:distro:debian:11", "packages": [ "poppler" ] }, { "reason": "changed", "id": "CVE-2022-24106", "namespace": "debian:distro:debian:12", "packages": [ "poppler" ] }, { "reason": "changed", "id": "CVE-2022-24106", "namespace": "debian:distro:debian:unstable", "packages": [ "poppler" ] }, { "reason": "changed", "id": "CVE-2022-2447", "namespace": "debian:distro:debian:12", "packages": [ "python-keystonemiddleware" ] }, { "reason": "added", "id": "CVE-2022-24697", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-2476", "namespace": "nvd:cpe", "packages": [ "wavpack" ] }, { "reason": "changed", "id": "CVE-2022-24795", "namespace": "debian:distro:debian:12", "packages": [ "ruby-yajl" ] }, { "reason": "changed", "id": "CVE-2022-24795", "namespace": "debian:distro:debian:unstable", "packages": [ "ruby-yajl" ] }, { "reason": "changed", "id": "CVE-2022-24836", "namespace": "nvd:cpe", "packages": [ "nokogiri" ] }, { "reason": "changed", "id": "CVE-2022-25235", "namespace": "redhat:distro:redhat:8", "packages": [ "expat", "firefox", "firefox:flatpak/firefox", "thunderbird", "thunderbird:flatpak/thunderbird", "xmlrpc-c" ] }, { "reason": "changed", "id": "CVE-2022-25236", "namespace": "redhat:distro:redhat:8", "packages": [ "expat", "firefox", "firefox:flatpak/firefox", "thunderbird", "thunderbird:flatpak/thunderbird", "xmlrpc-c" ] }, { "reason": "changed", "id": "CVE-2022-25315", "namespace": "redhat:distro:redhat:8", "packages": [ "expat", "firefox", "firefox:flatpak/firefox", "thunderbird", "thunderbird:flatpak/thunderbird" ] }, { "reason": "changed", "id": "CVE-2022-25648", "namespace": "nvd:cpe", "packages": [ "extra_packages_for_enterprise_linux", "git" ] }, { "reason": "changed", "id": "CVE-2022-26121", "namespace": "nvd:cpe", "packages": [ "fortianalyzer", "fortimanager" ] }, { "reason": "changed", "id": "CVE-2022-2625", "namespace": "redhat:distro:redhat:8", "packages": [ "postgresql", "postgresql:10/postgresql", "postgresql:12/postgresql", "postgresql:13/postgresql" ] }, { "reason": "added", "id": "CVE-2022-26305", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "libreoffice" ] }, { "reason": "added", "id": "CVE-2022-26306", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "libreoffice" ] }, { "reason": "added", "id": "CVE-2022-26307", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2022-26365", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-26365", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-26365", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-26373", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-26373", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-26373", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "added", "id": "CVE-2022-26505", "namespace": "alpine:distro:alpine:edge", "packages": [ "minidlna" ] }, { "reason": "changed", "id": "CVE-2022-2663", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-2720", "namespace": "nvd:cpe", "packages": [ "octopus_server" ] }, { "reason": "changed", "id": "CVE-2022-2764", "namespace": "nvd:cpe", "packages": [ "integration_camel_k", "jboss_enterprise_application_platform", "jboss_fuse", "single_sign-on", "undertow" ] }, { "reason": "changed", "id": "CVE-2022-27664", "namespace": "redhat:distro:redhat:8", "packages": [ "container-tools:3.0/buildah", "container-tools:3.0/containernetworking-plugins", "container-tools:3.0/podman", "container-tools:3.0/skopeo", "container-tools:3.0/toolbox", "container-tools:4.0/buildah", "container-tools:4.0/conmon", "container-tools:4.0/containernetworking-plugins", "container-tools:4.0/podman", "container-tools:4.0/skopeo", "container-tools:4.0/toolbox", "container-tools:rhel8/buildah", "container-tools:rhel8/conmon", "container-tools:rhel8/containernetworking-plugins", "container-tools:rhel8/podman", "container-tools:rhel8/skopeo", "container-tools:rhel8/toolbox", "git-lfs", "go-toolset:rhel8/golang", "grafana", "grafana-pcp", "osbuild-composer", "weldr-client" ] }, { "reason": "changed", "id": "CVE-2022-27664", "namespace": "redhat:distro:redhat:9", "packages": [ "buildah", "butane", "containernetworking-plugins", "git-lfs", "golang", "grafana", "grafana-pcp", "ignition", "osbuild-composer", "podman", "skopeo", "toolbox", "weldr-client" ] }, { "reason": "added", "id": "CVE-2022-2780", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-28193", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-28194", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-28195", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-28197", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-2828", "namespace": "nvd:cpe", "packages": [ "octopus_server" ] }, { "reason": "changed", "id": "CVE-2022-28327", "namespace": "nvd:cpe", "packages": [ "extra_packages_for_enterprise_linux", "go" ] }, { "reason": "changed", "id": "CVE-2022-28346", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-28346", "namespace": "nvd:cpe", "packages": [ "django" ] }, { "reason": "changed", "id": "CVE-2022-28347", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-28347", "namespace": "nvd:cpe", "packages": [ "django" ] }, { "reason": "changed", "id": "CVE-2022-2850", "namespace": "debian:distro:debian:10", "packages": [ "389-ds-base" ] }, { "reason": "changed", "id": "CVE-2022-2850", "namespace": "debian:distro:debian:11", "packages": [ "389-ds-base" ] }, { "reason": "changed", "id": "CVE-2022-2850", "namespace": "debian:distro:debian:12", "packages": [ "389-ds-base" ] }, { "reason": "changed", "id": "CVE-2022-2850", "namespace": "debian:distro:debian:unstable", "packages": [ "389-ds-base" ] }, { "reason": "added", "id": "CVE-2022-2850", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-28697", "namespace": "nvd:cpe", "packages": [ "active_management_technology", "standard_manageability" ] }, { "reason": "added", "id": "CVE-2022-28759", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-28760", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-28761", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-28762", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-2879", "namespace": "debian:distro:debian:10", "packages": [ "golang-1.11" ] }, { "reason": "changed", "id": "CVE-2022-2879", "namespace": "debian:distro:debian:11", "packages": [ "golang-1.15" ] }, { "reason": "changed", "id": "CVE-2022-2879", "namespace": "debian:distro:debian:12", "packages": [ "golang-1.18", "golang-1.19" ] }, { "reason": "changed", "id": "CVE-2022-2879", "namespace": "debian:distro:debian:unstable", "packages": [ "golang-1.18", "golang-1.19" ] }, { "reason": "added", "id": "CVE-2022-2879", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-2879", "namespace": "redhat:distro:redhat:8", "packages": [ "container-tools:3.0/buildah", "container-tools:3.0/podman", "container-tools:3.0/skopeo", "container-tools:4.0/buildah", "container-tools:4.0/podman", "container-tools:4.0/skopeo", "container-tools:rhel8/buildah", "container-tools:rhel8/podman", "container-tools:rhel8/skopeo", "go-toolset:rhel8/golang", "osbuild-composer", "weldr-client" ] }, { "reason": "added", "id": "CVE-2022-2879", "namespace": "redhat:distro:redhat:9", "packages": [ "buildah", "golang", "osbuild-composer", "podman", "skopeo", "weldr-client" ] }, { "reason": "changed", "id": "CVE-2022-2880", "namespace": "debian:distro:debian:10", "packages": [ "golang-1.11" ] }, { "reason": "changed", "id": "CVE-2022-2880", "namespace": "debian:distro:debian:11", "packages": [ "golang-1.15" ] }, { "reason": "changed", "id": "CVE-2022-2880", "namespace": "debian:distro:debian:12", "packages": [ "golang-1.18", "golang-1.19" ] }, { "reason": "changed", "id": "CVE-2022-2880", "namespace": "debian:distro:debian:unstable", "packages": [ "golang-1.18", "golang-1.19" ] }, { "reason": "added", "id": "CVE-2022-2880", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-2880", "namespace": "redhat:distro:redhat:8", "packages": [ "container-tools:3.0/buildah", "container-tools:3.0/podman", "container-tools:3.0/skopeo", "container-tools:4.0/buildah", "container-tools:4.0/podman", "container-tools:4.0/skopeo", "container-tools:rhel8/buildah", "container-tools:rhel8/podman", "container-tools:rhel8/skopeo", "git-lfs", "go-toolset:rhel8/golang", "grafana", "grafana-pcp", "osbuild-composer" ] }, { "reason": "added", "id": "CVE-2022-2880", "namespace": "redhat:distro:redhat:9", "packages": [ "buildah", "git-lfs", "golang", "grafana", "grafana-pcp", "ignition", "osbuild-composer", "podman", "skopeo" ] }, { "reason": "changed", "id": "CVE-2022-28866", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-28887", "namespace": "nvd:cpe", "packages": [ "atlant", "elements_endpoint_detection_and_response", "elements_endpoint_protection", "internet_gatekeeper", "linux_security", "linux_security_64" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "alpine:distro:alpine:3.13", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "alpine:distro:alpine:3.14", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "alpine:distro:alpine:3.15", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "alpine:distro:alpine:3.16", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "alpine:distro:alpine:edge", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "debian:distro:debian:10", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "debian:distro:debian:11", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "debian:distro:debian:12", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "debian:distro:debian:unstable", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2928", "namespace": "nvd:cpe", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "alpine:distro:alpine:3.13", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "alpine:distro:alpine:3.14", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "alpine:distro:alpine:3.15", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "alpine:distro:alpine:3.16", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "alpine:distro:alpine:edge", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "debian:distro:debian:10", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "debian:distro:debian:11", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "debian:distro:debian:12", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "debian:distro:debian:unstable", "packages": [ "isc-dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2929", "namespace": "nvd:cpe", "packages": [ "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-2953", "namespace": "nvd:cpe", "packages": [ "libtiff" ] }, { "reason": "added", "id": "CVE-2022-2953", "namespace": "redhat:distro:redhat:6", "packages": [ "libtiff" ] }, { "reason": "added", "id": "CVE-2022-2953", "namespace": "redhat:distro:redhat:7", "packages": [ "compat-libtiff3", "libtiff" ] }, { "reason": "added", "id": "CVE-2022-2953", "namespace": "redhat:distro:redhat:8", "packages": [ "compat-libtiff3", "libtiff" ] }, { "reason": "added", "id": "CVE-2022-2953", "namespace": "redhat:distro:redhat:9", "packages": [ "libtiff" ] }, { "reason": "changed", "id": "CVE-2022-2962", "namespace": "debian:distro:debian:10", "packages": [ "qemu" ] }, { "reason": "added", "id": "CVE-2022-2963", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-2963", "namespace": "redhat:distro:redhat:8", "packages": [ "jasper" ] }, { "reason": "changed", "id": "CVE-2022-2963", "namespace": "redhat:distro:redhat:9", "packages": [ "jasper" ] }, { "reason": "removed", "id": "CVE-2022-2964", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "changed", "id": "CVE-2022-2981", "namespace": "nvd:cpe", "packages": [ "download_monitor" ] }, { "reason": "added", "id": "CVE-2022-2984", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-2985", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-30601", "namespace": "nvd:cpe", "packages": [ "active_management_technology", "standard_manageability" ] }, { "reason": "changed", "id": "CVE-2022-30614", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "changed", "id": "CVE-2022-30763", "namespace": "alpine:distro:alpine:edge", "packages": [ "janet" ] }, { "reason": "changed", "id": "CVE-2022-30763", "namespace": "nvd:cpe", "packages": [ "janet" ] }, { "reason": "changed", "id": "CVE-2022-30944", "namespace": "nvd:cpe", "packages": [ "active_management_technology", "standard_manageability" ] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "nvd:cpe", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "redhat:distro:redhat:8", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "redhat:distro:redhat:9", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31123", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-31129", "namespace": "nvd:cpe", "packages": [ "moment" ] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "redhat:distro:redhat:8", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "redhat:distro:redhat:9", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-31130", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-3116", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "heimdal" ] }, { "reason": "changed", "id": "CVE-2022-3116", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "heimdal" ] }, { "reason": "added", "id": "CVE-2022-31228", "namespace": "nvd:cpe", "packages": [ "xtremio_management_server" ] }, { "reason": "changed", "id": "CVE-2022-3136", "namespace": "nvd:cpe", "packages": [ "social_rocket" ] }, { "reason": "changed", "id": "CVE-2022-3137", "namespace": "nvd:cpe", "packages": [ "taskbuilder" ] }, { "reason": "changed", "id": "CVE-2022-3140", "namespace": "debian:distro:debian:10", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2022-3140", "namespace": "debian:distro:debian:11", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2022-3140", "namespace": "debian:distro:debian:12", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2022-3140", "namespace": "debian:distro:debian:unstable", "packages": [ "libreoffice" ] }, { "reason": "changed", "id": "CVE-2022-3140", "namespace": "nvd:cpe", "packages": [ "libreoffice" ] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "redhat:distro:redhat:7", "packages": [ "libreoffice" ] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "redhat:distro:redhat:8", "packages": [ "libreoffice", "libreoffice:flatpak/libreoffice" ] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "redhat:distro:redhat:9", "packages": [ "libreoffice", "libreoffice:flatpak/libreoffice" ] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3140", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-3154", "namespace": "nvd:cpe", "packages": [ "integration_for_billingo_&_gravity_forms", "integration_for_szamlazz.hu_&_gravity_forms", "woo_billingo_plus" ] }, { "reason": "changed", "id": "CVE-2022-31682", "namespace": "nvd:cpe", "packages": [ "vrealize_operations" ] }, { "reason": "changed", "id": "CVE-2022-3171", "namespace": "debian:distro:debian:10", "packages": [ "protobuf" ] }, { "reason": "changed", "id": "CVE-2022-3171", "namespace": "debian:distro:debian:11", "packages": [ "protobuf" ] }, { "reason": "changed", "id": "CVE-2022-3171", "namespace": "debian:distro:debian:12", "packages": [ "protobuf" ] }, { "reason": "changed", "id": "CVE-2022-3171", "namespace": "debian:distro:debian:unstable", "packages": [ "protobuf" ] }, { "reason": "added", "id": "CVE-2022-3171", "namespace": "nvd:cpe", "packages": [ "google-protobuf", "protobuf-java", "protobuf-javalite", "protobuf-kotlin", "protobuf-kotlin-lite" ] }, { "reason": "changed", "id": "CVE-2022-31765", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-31766", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-3176", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux-aws-5.4", "linux-azure-5.4", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-oracle-5.4", "linux-raspi-5.4" ] }, { "reason": "changed", "id": "CVE-2022-3176", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-3176", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-3207", "namespace": "nvd:cpe", "packages": [ "simple-file-list" ] }, { "reason": "changed", "id": "CVE-2022-3208", "namespace": "nvd:cpe", "packages": [ "simple-file-list" ] }, { "reason": "changed", "id": "CVE-2022-3209", "namespace": "nvd:cpe", "packages": [ "soledad" ] }, { "reason": "added", "id": "CVE-2022-32149", "namespace": "debian:distro:debian:11", "packages": [ "golang-golang-x-text" ] }, { "reason": "added", "id": "CVE-2022-32149", "namespace": "debian:distro:debian:12", "packages": [ "golang-golang-x-text" ] }, { "reason": "added", "id": "CVE-2022-32149", "namespace": "debian:distro:debian:unstable", "packages": [ "golang-golang-x-text" ] }, { "reason": "added", "id": "CVE-2022-32149", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-32175", "namespace": "nvd:cpe", "packages": [ "adguardhome" ] }, { "reason": "added", "id": "CVE-2022-32177", "namespace": "nvd:cpe", "packages": [ "gin-vue-admin" ] }, { "reason": "changed", "id": "CVE-2022-3220", "namespace": "nvd:cpe", "packages": [ "advanced_comment_form" ] }, { "reason": "changed", "id": "CVE-2022-32296", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-gke-5.4", "linux-gkeop-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-3234", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "changed", "id": "CVE-2022-3235", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "added", "id": "CVE-2022-32483", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32484", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32485", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-32486", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32487", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32488", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32489", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32491", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-32492", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-32493", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-3256", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "changed", "id": "CVE-2022-32589", "namespace": "nvd:cpe", "packages": [ "yocto" ] }, { "reason": "changed", "id": "CVE-2022-32590", "namespace": "nvd:cpe", "packages": [ "yocto" ] }, { "reason": "changed", "id": "CVE-2022-32591", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-32592", "namespace": "nvd:cpe", "packages": [ "yocto" ] }, { "reason": "changed", "id": "CVE-2022-32593", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-3278", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "changed", "id": "CVE-2022-3296", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "changed", "id": "CVE-2022-3297", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "added", "id": "CVE-2022-33106", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-3324", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "changed", "id": "CVE-2022-3352", "namespace": "nvd:cpe", "packages": [ "vim" ] }, { "reason": "changed", "id": "CVE-2022-3358", "namespace": "alpine:distro:alpine:3.15", "packages": [ "openssl3" ] }, { "reason": "changed", "id": "CVE-2022-3358", "namespace": "alpine:distro:alpine:3.16", "packages": [ "openssl3" ] }, { "reason": "changed", "id": "CVE-2022-3358", "namespace": "alpine:distro:alpine:edge", "packages": [ "openssl" ] }, { "reason": "changed", "id": "CVE-2022-3358", "namespace": "debian:distro:debian:12", "packages": [ "openssl" ] }, { "reason": "changed", "id": "CVE-2022-3358", "namespace": "debian:distro:debian:unstable", "packages": [ "openssl" ] }, { "reason": "changed", "id": "CVE-2022-3358", "namespace": "nvd:cpe", "packages": [ "openssl" ] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "redhat:distro:redhat:6", "packages": [ "openssl" ] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "redhat:distro:redhat:7", "packages": [ "openssl" ] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "redhat:distro:redhat:9", "packages": [ "openssl" ] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3358", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-33639", "namespace": "nvd:cpe", "packages": [ "edge_chromium" ] }, { "reason": "changed", "id": "CVE-2022-33740", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-33740", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-33740", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-33741", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-33741", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-33741", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-33742", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-33742", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-33742", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-33743", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-33744", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-33744", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gcp-5.15", "linux-gke", "linux-gke-5.15", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-33744", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-33746", "namespace": "debian:distro:debian:11", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33746", "namespace": "debian:distro:debian:12", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33746", "namespace": "debian:distro:debian:unstable", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33746", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-33746", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33746", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33746", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33746", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33746", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-33747", "namespace": "debian:distro:debian:11", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33747", "namespace": "debian:distro:debian:12", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33747", "namespace": "debian:distro:debian:unstable", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33747", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-33747", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33747", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33747", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33747", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33747", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-33748", "namespace": "debian:distro:debian:11", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33748", "namespace": "debian:distro:debian:12", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33748", "namespace": "debian:distro:debian:unstable", "packages": [ "xen" ] }, { "reason": "changed", "id": "CVE-2022-33748", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-33748", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33748", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33748", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33748", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-33748", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-33749", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-33890", "namespace": "nvd:cpe", "packages": [ "autocad", "autocad_advance_steel", "autocad_architecture", "autocad_civil_3d", "autocad_electrical", "autocad_lt", "autocad_map_3d", "autocad_mechanical", "autocad_mep", "autocad_plant_3d", "design_review" ] }, { "reason": "added", "id": "CVE-2022-33918", "namespace": "nvd:cpe", "packages": [ "geodrive" ] }, { "reason": "added", "id": "CVE-2022-33919", "namespace": "nvd:cpe", "packages": [ "geodrive" ] }, { "reason": "added", "id": "CVE-2022-33920", "namespace": "nvd:cpe", "packages": [ "geodrive" ] }, { "reason": "added", "id": "CVE-2022-33921", "namespace": "nvd:cpe", "packages": [ "geodrive" ] }, { "reason": "added", "id": "CVE-2022-33922", "namespace": "nvd:cpe", "packages": [ "geodrive" ] }, { "reason": "added", "id": "CVE-2022-33937", "namespace": "nvd:cpe", "packages": [ "geodrive" ] }, { "reason": "changed", "id": "CVE-2022-33978", "namespace": "nvd:cpe", "packages": [ "fontmeister" ] }, { "reason": "added", "id": "CVE-2022-34020", "namespace": "nvd:cpe", "packages": [ "iot_platform_and_lorawan_network_server" ] }, { "reason": "added", "id": "CVE-2022-34021", "namespace": "nvd:cpe", "packages": [ "iot_platform_and_lorawan_network_server" ] }, { "reason": "added", "id": "CVE-2022-34022", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-34265", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-34265", "namespace": "nvd:cpe", "packages": [ "django" ] }, { "reason": "changed", "id": "CVE-2022-34326", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-34334", "namespace": "nvd:cpe", "packages": [ "sterling_partner_engagement_manager" ] }, { "reason": "changed", "id": "CVE-2022-3435", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-34390", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-34391", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-3439", "namespace": "nvd:cpe", "packages": [ "rdiffweb" ] }, { "reason": "changed", "id": "CVE-2022-34402", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-34425", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-34426", "namespace": "nvd:cpe", "packages": [ "container_storage_modules" ] }, { "reason": "changed", "id": "CVE-2022-34427", "namespace": "nvd:cpe", "packages": [ "container_storage_modules" ] }, { "reason": "changed", "id": "CVE-2022-34430", "namespace": "nvd:cpe", "packages": [ "hybrid_client" ] }, { "reason": "changed", "id": "CVE-2022-34431", "namespace": "nvd:cpe", "packages": [ "hybrid_client" ] }, { "reason": "changed", "id": "CVE-2022-34432", "namespace": "nvd:cpe", "packages": [ "hybrid_client" ] }, { "reason": "changed", "id": "CVE-2022-34434", "namespace": "nvd:cpe", "packages": [ "cloud_mobility_for_dell_emc_storage" ] }, { "reason": "changed", "id": "CVE-2022-3445", "namespace": "debian:distro:debian:11", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3445", "namespace": "debian:distro:debian:12", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3445", "namespace": "debian:distro:debian:unstable", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3446", "namespace": "debian:distro:debian:11", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3446", "namespace": "debian:distro:debian:12", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3446", "namespace": "debian:distro:debian:unstable", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3447", "namespace": "debian:distro:debian:11", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3447", "namespace": "debian:distro:debian:12", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3447", "namespace": "debian:distro:debian:unstable", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3448", "namespace": "debian:distro:debian:11", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3448", "namespace": "debian:distro:debian:12", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3448", "namespace": "debian:distro:debian:unstable", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-34494", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-34495", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-3449", "namespace": "debian:distro:debian:11", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3449", "namespace": "debian:distro:debian:12", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3449", "namespace": "debian:distro:debian:unstable", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3450", "namespace": "debian:distro:debian:11", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3450", "namespace": "debian:distro:debian:12", "packages": [ "chromium" ] }, { "reason": "changed", "id": "CVE-2022-3450", "namespace": "debian:distro:debian:unstable", "packages": [ "chromium" ] }, { "reason": "added", "id": "CVE-2022-3456", "namespace": "nvd:cpe", "packages": [ "rdiffweb" ] }, { "reason": "added", "id": "CVE-2022-3457", "namespace": "nvd:cpe", "packages": [ "rdiffweb" ] }, { "reason": "added", "id": "CVE-2022-3458", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3464", "namespace": "nvd:cpe", "packages": [ "puppycms" ] }, { "reason": "added", "id": "CVE-2022-3465", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-3467", "namespace": "nvd:cpe", "packages": [ "jiusi_oa" ] }, { "reason": "added", "id": "CVE-2022-3470", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3471", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3472", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3473", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "debian:distro:debian:10", "packages": [ "nss" ] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "debian:distro:debian:11", "packages": [ "nss" ] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "debian:distro:debian:12", "packages": [ "nss" ] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "debian:distro:debian:unstable", "packages": [ "nss" ] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3479", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-3492", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3493", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3495", "namespace": "nvd:cpe", "packages": [ "simple_online_public_access_catalog" ] }, { "reason": "added", "id": "CVE-2022-3496", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3497", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3502", "namespace": "nvd:cpe", "packages": [ "human_resource_management_system" ] }, { "reason": "added", "id": "CVE-2022-3503", "namespace": "nvd:cpe", "packages": [ "purchase_order_management_system" ] }, { "reason": "added", "id": "CVE-2022-35040", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35040", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35040", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35040", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35040", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35041", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35041", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35041", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35041", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35041", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35042", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35042", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35042", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35042", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35042", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35043", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35043", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35043", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35043", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35043", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35044", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35044", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35044", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35044", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35044", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35045", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35045", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35045", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35045", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35045", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35046", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35046", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35046", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35046", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35046", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35047", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35047", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35047", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35047", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35047", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35048", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35048", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35048", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35048", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35048", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35049", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35049", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35049", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35049", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35049", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-3504", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35050", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35050", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35050", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35050", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35050", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35051", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35051", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35051", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35051", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35051", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35052", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35052", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35052", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35052", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35052", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35053", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35053", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35053", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35053", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35053", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35054", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35054", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35054", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35054", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35054", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35055", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35055", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35055", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35055", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35055", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35056", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35056", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35056", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35056", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35056", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35058", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35058", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35058", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35058", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35058", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-35059", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-35059", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-35059", "namespace": "debian:distro:debian:12", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35059", "namespace": "debian:distro:debian:unstable", "packages": [ "texlive-bin" ] }, { "reason": "added", "id": "CVE-2022-35059", "namespace": "nvd:cpe", "packages": [ "otfcc" ] }, { "reason": "added", "id": "CVE-2022-3505", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-3506", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35080", "namespace": "nvd:cpe", "packages": [ "swftools" ] }, { "reason": "added", "id": "CVE-2022-35080", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35080", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35080", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35080", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35080", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35081", "namespace": "nvd:cpe", "packages": [ "swftools" ] }, { "reason": "added", "id": "CVE-2022-35081", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35081", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35081", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35081", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35081", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-35134", "namespace": "nvd:cpe", "packages": [ "iot_platform" ] }, { "reason": "added", "id": "CVE-2022-35135", "namespace": "nvd:cpe", "packages": [ "iot_platform" ] }, { "reason": "added", "id": "CVE-2022-35136", "namespace": "nvd:cpe", "packages": [ "iot_platform" ] }, { "reason": "added", "id": "CVE-2022-3517", "namespace": "redhat:distro:redhat:8", "packages": [ "389-ds:1.4/389-ds-base", "cockpit", "cockpit-appstream", "grafana", "mozjs60", "nodejs:14/nodejs", "nodejs:14/nodejs-nodemon", "nodejs:16/nodejs", "nodejs:16/nodejs-nodemon", "nodejs:18/nodejs", "nodejs:18/nodejs-nodemon", "pcs", "ruby:3.1/ruby" ] }, { "reason": "added", "id": "CVE-2022-3517", "namespace": "redhat:distro:redhat:9", "packages": [ "389-ds-base", "gjs", "grafana", "nodejs", "nodejs-nodemon", "nodejs:18/nodejs", "nodejs:18/nodejs-nodemon", "polkit", "rust" ] }, { "reason": "added", "id": "CVE-2022-3518", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-3519", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-3521", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3521", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3521", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3521", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-35226", "namespace": "nvd:cpe", "packages": [ "data_services" ] }, { "reason": "added", "id": "CVE-2022-3522", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3522", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3522", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3522", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3523", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3523", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3523", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3523", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3524", "namespace": "debian:distro:debian:10", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3524", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3524", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3524", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "changed", "id": "CVE-2022-35255", "namespace": "debian:distro:debian:12", "packages": [ "nodejs" ] }, { "reason": "changed", "id": "CVE-2022-35255", "namespace": "redhat:distro:redhat:9", "packages": [ "nodejs", "nodejs:18/nodejs" ] }, { "reason": "changed", "id": "CVE-2022-35256", "namespace": "debian:distro:debian:12", "packages": [ "nodejs" ] }, { "reason": "added", "id": "CVE-2022-3526", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-3526", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "added", "id": "CVE-2022-3526", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3526", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-3527", "namespace": "debian:distro:debian:10", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3527", "namespace": "debian:distro:debian:11", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3527", "namespace": "debian:distro:debian:12", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3527", "namespace": "debian:distro:debian:unstable", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3528", "namespace": "debian:distro:debian:10", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3528", "namespace": "debian:distro:debian:11", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3528", "namespace": "debian:distro:debian:12", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3528", "namespace": "debian:distro:debian:unstable", "packages": [ "iproute2" ] }, { "reason": "changed", "id": "CVE-2022-35296", "namespace": "nvd:cpe", "packages": [ "businessobjects_business_intelligence" ] }, { "reason": "changed", "id": "CVE-2022-35297", "namespace": "nvd:cpe", "packages": [ "enable_now" ] }, { "reason": "changed", "id": "CVE-2022-35299", "namespace": "nvd:cpe", "packages": [ "sap_iq", "sql_anywhere" ] }, { "reason": "added", "id": "CVE-2022-3529", "namespace": "debian:distro:debian:10", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3529", "namespace": "debian:distro:debian:11", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3529", "namespace": "debian:distro:debian:12", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3529", "namespace": "debian:distro:debian:unstable", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3530", "namespace": "debian:distro:debian:10", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3530", "namespace": "debian:distro:debian:11", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3530", "namespace": "debian:distro:debian:12", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-3530", "namespace": "debian:distro:debian:unstable", "packages": [ "iproute2" ] }, { "reason": "added", "id": "CVE-2022-35611", "namespace": "nvd:cpe", "packages": [ "mqttroute" ] }, { "reason": "added", "id": "CVE-2022-35612", "namespace": "nvd:cpe", "packages": [ "mqttroute" ] }, { "reason": "added", "id": "CVE-2022-35689", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35690", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35691", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35698", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35710", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35711", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-35712", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-35829", "namespace": "nvd:cpe", "packages": [ "azure_service_fabric" ] }, { "reason": "added", "id": "CVE-2022-35944", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36063", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36067", "namespace": "nvd:cpe", "packages": [ "vm2" ] }, { "reason": "changed", "id": "CVE-2022-36109", "namespace": "debian:distro:debian:unstable", "packages": [ "docker.io" ] }, { "reason": "added", "id": "CVE-2022-36280", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-36280", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-36280", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-36280", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "changed", "id": "CVE-2022-36359", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-36359", "namespace": "nvd:cpe", "packages": [ "django" ] }, { "reason": "changed", "id": "CVE-2022-36360", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36361", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36362", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36363", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-36402", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-36402", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-36402", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-36402", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "changed", "id": "CVE-2022-36773", "namespace": "nvd:cpe", "packages": [ "cognos_analytics" ] }, { "reason": "added", "id": "CVE-2022-36802", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-36803", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36879", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "linux", "linux-aws", "linux-aws-5.4", "linux-azure-4.15", "linux-azure-5.4", "linux-dell300x", "linux-gcp-4.15", "linux-gcp-5.4", "linux-hwe-5.4", "linux-ibm-5.4", "linux-kvm", "linux-oracle", "linux-oracle-5.4", "linux-raspi-5.4", "linux-raspi2", "linux-snapdragon" ] }, { "reason": "changed", "id": "CVE-2022-36879", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "linux", "linux-aws", "linux-aws-5.15", "linux-azure", "linux-azure-5.15", "linux-azure-fde", "linux-bluefield", "linux-gcp", "linux-gke", "linux-gkeop", "linux-hwe-5.15", "linux-ibm", "linux-intel-iotg-5.15", "linux-kvm", "linux-lowlatency-hwe-5.15", "linux-oem-5.14", "linux-oracle", "linux-raspi" ] }, { "reason": "changed", "id": "CVE-2022-36879", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-36944", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36944", "namespace": "debian:distro:debian:11", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36944", "namespace": "debian:distro:debian:12", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36944", "namespace": "debian:distro:debian:unstable", "packages": [] }, { "reason": "changed", "id": "CVE-2022-36944", "namespace": "nvd:cpe", "packages": [ "scala" ] }, { "reason": "changed", "id": "CVE-2022-36946", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "changed", "id": "CVE-2022-37026", "namespace": "debian:distro:debian:10", "packages": [ "erlang" ] }, { "reason": "added", "id": "CVE-2022-37208", "namespace": "nvd:cpe", "packages": [ "jfinal_cms" ] }, { "reason": "changed", "id": "CVE-2022-37599", "namespace": "nvd:cpe", "packages": [ "loader-utils" ] }, { "reason": "added", "id": "CVE-2022-37599", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37599", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37599", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37599", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37599", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37601", "namespace": "nvd:cpe", "packages": [ "loader-utils" ] }, { "reason": "added", "id": "CVE-2022-37601", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37601", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37601", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37601", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37601", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37602", "namespace": "nvd:cpe", "packages": [ "grunt-karma" ] }, { "reason": "added", "id": "CVE-2022-37603", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-37609", "namespace": "nvd:cpe", "packages": [ "js-beautify" ] }, { "reason": "added", "id": "CVE-2022-37609", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37609", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-37609", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [ "thunderbird" ] }, { "reason": "added", "id": "CVE-2022-37609", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [ "thunderbird" ] }, { "reason": "added", "id": "CVE-2022-37609", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "thunderbird" ] }, { "reason": "changed", "id": "CVE-2022-37611", "namespace": "nvd:cpe", "packages": [ "gh-pages" ] }, { "reason": "added", "id": "CVE-2022-37614", "namespace": "nvd:cpe", "packages": [ "mockery" ] }, { "reason": "changed", "id": "CVE-2022-37616", "namespace": "debian:distro:debian:12", "packages": [ "node-xmldom" ] }, { "reason": "changed", "id": "CVE-2022-37616", "namespace": "nvd:cpe", "packages": [ "xmldom" ] }, { "reason": "changed", "id": "CVE-2022-37617", "namespace": "nvd:cpe", "packages": [ "browserify-shim" ] }, { "reason": "changed", "id": "CVE-2022-37864", "namespace": "nvd:cpe", "packages": [ "solid_edge" ] }, { "reason": "added", "id": "CVE-2022-37968", "namespace": "nvd:cpe", "packages": [ "azure_arc-enabled_kubernetes", "azure_stack_edge" ] }, { "reason": "added", "id": "CVE-2022-37971", "namespace": "nvd:cpe", "packages": [ "malware_protection_engine" ] }, { "reason": "changed", "id": "CVE-2022-37973", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-37975", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38001", "namespace": "nvd:cpe", "packages": [ "365_apps", "office", "office_long_term_servicing_channel" ] }, { "reason": "changed", "id": "CVE-2022-38022", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-38032", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-38034", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-38042", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-38043", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-38045", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-38046", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38048", "namespace": "nvd:cpe", "packages": [ "365_apps", "office", "office_long_term_servicing_channel" ] }, { "reason": "added", "id": "CVE-2022-38049", "namespace": "nvd:cpe", "packages": [ "365_apps", "office", "office_long_term_servicing_channel" ] }, { "reason": "added", "id": "CVE-2022-38053", "namespace": "nvd:cpe", "packages": [ "sharepoint_enterprise_server", "sharepoint_foundation", "sharepoint_server" ] }, { "reason": "changed", "id": "CVE-2022-38086", "namespace": "nvd:cpe", "packages": [ "shortcodes_ultimate" ] }, { "reason": "added", "id": "CVE-2022-38096", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-38096", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-38096", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-38096", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "changed", "id": "CVE-2022-38177", "namespace": "redhat:distro:redhat:9", "packages": [ "bind", "dhcp" ] }, { "reason": "changed", "id": "CVE-2022-38339", "namespace": "nvd:cpe", "packages": [ "fme_server" ] }, { "reason": "changed", "id": "CVE-2022-38340", "namespace": "nvd:cpe", "packages": [ "fme_server" ] }, { "reason": "changed", "id": "CVE-2022-38341", "namespace": "nvd:cpe", "packages": [ "fme_server" ] }, { "reason": "changed", "id": "CVE-2022-38342", "namespace": "nvd:cpe", "packages": [ "fme_server" ] }, { "reason": "changed", "id": "CVE-2022-38371", "namespace": "nvd:cpe", "packages": [ "nucleus_net", "nucleus_readystart_v3", "nucleus_source_code" ] }, { "reason": "changed", "id": "CVE-2022-38388", "namespace": "nvd:cpe", "packages": [ "navigator_mobile" ] }, { "reason": "added", "id": "CVE-2022-38418", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38419", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38420", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38421", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38422", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38423", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38424", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38437", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38440", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38441", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38442", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38443", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38444", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38445", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38446", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38447", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38448", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38449", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38450", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38457", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-38457", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-38457", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-38457", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "changed", "id": "CVE-2022-38465", "namespace": "nvd:cpe", "packages": [ "simatic_s7-1500_software_controller" ] }, { "reason": "changed", "id": "CVE-2022-38537", "namespace": "nvd:cpe", "packages": [ "archery" ] }, { "reason": "changed", "id": "CVE-2022-38541", "namespace": "nvd:cpe", "packages": [ "archery" ] }, { "reason": "added", "id": "CVE-2022-38669", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38670", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38671", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38672", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38673", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38676", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38677", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38679", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38687", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38688", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38689", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38690", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38697", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38698", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38902", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38977", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38980", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38981", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38982", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38983", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38984", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38985", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38986", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-38998", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39002", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39011", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39013", "namespace": "nvd:cpe", "packages": [ "business_objects_business_intelligence_platform" ] }, { "reason": "changed", "id": "CVE-2022-39015", "namespace": "nvd:cpe", "packages": [ "business_objects_business_intelligence_platform" ] }, { "reason": "added", "id": "CVE-2022-39064", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39065", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39080", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39103", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39105", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39107", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39108", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39109", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39110", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39111", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39112", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39113", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39114", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39115", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39117", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39120", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39121", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39122", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39123", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39124", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39125", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39126", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39127", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39128", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39189", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [ "linux", "linux-aws", "linux-azure", "linux-azure-fde", "linux-gcp", "linux-gke", "linux-gkeop", "linux-ibm", "linux-intel-iotg", "linux-kvm", "linux-lowlatency", "linux-oem-5.17", "linux-oracle", "linux-raspi", "linux-riscv" ] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "redhat:distro:redhat:8", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "redhat:distro:redhat:9", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39201", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "redhat:distro:redhat:8", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "redhat:distro:redhat:9", "packages": [ "grafana" ] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39229", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39271", "namespace": "nvd:cpe", "packages": [ "traefik" ] }, { "reason": "added", "id": "CVE-2022-39278", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "debian:distro:debian:10", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "debian:distro:debian:11", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "debian:distro:debian:12", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "debian:distro:debian:unstable", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "nvd:cpe", "packages": [ "freerdp" ] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39282", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "debian:distro:debian:10", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "debian:distro:debian:11", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "debian:distro:debian:12", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "debian:distro:debian:unstable", "packages": [ "freerdp2" ] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "nvd:cpe", "packages": [ "freerdp" ] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-39283", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39288", "namespace": "nvd:cpe", "packages": [ "fastify" ] }, { "reason": "added", "id": "CVE-2022-39293", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39295", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39296", "namespace": "nvd:cpe", "packages": [ "melis-asset-manager" ] }, { "reason": "added", "id": "CVE-2022-39297", "namespace": "nvd:cpe", "packages": [ "meliscms" ] }, { "reason": "added", "id": "CVE-2022-39298", "namespace": "nvd:cpe", "packages": [ "meliscms" ] }, { "reason": "added", "id": "CVE-2022-39299", "namespace": "nvd:cpe", "packages": [ "passport-saml" ] }, { "reason": "added", "id": "CVE-2022-39300", "namespace": "nvd:cpe", "packages": [ "node_saml" ] }, { "reason": "added", "id": "CVE-2022-39302", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39303", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39308", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39309", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39310", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-39311", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-39800", "namespace": "nvd:cpe", "packages": [ "businessobjects_business_intelligence" ] }, { "reason": "changed", "id": "CVE-2022-39802", "namespace": "nvd:cpe", "packages": [ "manufacturing_execution" ] }, { "reason": "changed", "id": "CVE-2022-39803", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-39804", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-39805", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-39806", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-39807", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-39808", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-39955", "namespace": "nvd:cpe", "packages": [ "owasp_modsecurity_core_rule_set" ] }, { "reason": "changed", "id": "CVE-2022-39956", "namespace": "nvd:cpe", "packages": [ "owasp_modsecurity_core_rule_set" ] }, { "reason": "changed", "id": "CVE-2022-39957", "namespace": "nvd:cpe", "packages": [ "owasp_modsecurity_core_rule_set" ] }, { "reason": "changed", "id": "CVE-2022-39958", "namespace": "nvd:cpe", "packages": [ "owasp_modsecurity_core_rule_set" ] }, { "reason": "added", "id": "CVE-2022-40023", "namespace": "redhat:distro:redhat:6", "packages": [ "python-mako" ] }, { "reason": "added", "id": "CVE-2022-40023", "namespace": "redhat:distro:redhat:7", "packages": [ "python-mako", "resource-agents" ] }, { "reason": "added", "id": "CVE-2022-40023", "namespace": "redhat:distro:redhat:8", "packages": [ "python-mako", "resource-agents" ] }, { "reason": "added", "id": "CVE-2022-40023", "namespace": "redhat:distro:redhat:9", "packages": [ "python-mako" ] }, { "reason": "changed", "id": "CVE-2022-40047", "namespace": "nvd:cpe", "packages": [ "flatpress" ] }, { "reason": "added", "id": "CVE-2022-40133", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-40133", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-40133", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-40133", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "changed", "id": "CVE-2022-40147", "namespace": "nvd:cpe", "packages": [ "industrial_edge_management" ] }, { "reason": "changed", "id": "CVE-2022-40176", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40177", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40178", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40179", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40180", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40181", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40182", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-40187", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40226", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40227", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-40303", "namespace": "alpine:distro:alpine:3.13", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40303", "namespace": "alpine:distro:alpine:3.14", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40303", "namespace": "alpine:distro:alpine:3.15", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40303", "namespace": "alpine:distro:alpine:3.16", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40303", "namespace": "alpine:distro:alpine:edge", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40304", "namespace": "alpine:distro:alpine:3.13", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40304", "namespace": "alpine:distro:alpine:3.14", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40304", "namespace": "alpine:distro:alpine:3.15", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40304", "namespace": "alpine:distro:alpine:3.16", "packages": [ "libxml2" ] }, { "reason": "added", "id": "CVE-2022-40304", "namespace": "alpine:distro:alpine:edge", "packages": [ "libxml2" ] }, { "reason": "changed", "id": "CVE-2022-40440", "namespace": "nvd:cpe", "packages": [ "mxgraph" ] }, { "reason": "changed", "id": "CVE-2022-40468", "namespace": "debian:distro:debian:10", "packages": [ "tinyproxy" ] }, { "reason": "changed", "id": "CVE-2022-40469", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40494", "namespace": "nvd:cpe", "packages": [ "nps" ] }, { "reason": "changed", "id": "CVE-2022-40631", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40664", "namespace": "debian:distro:debian:10", "packages": [ "shiro" ] }, { "reason": "changed", "id": "CVE-2022-40664", "namespace": "debian:distro:debian:11", "packages": [ "shiro" ] }, { "reason": "changed", "id": "CVE-2022-40664", "namespace": "debian:distro:debian:12", "packages": [ "shiro" ] }, { "reason": "changed", "id": "CVE-2022-40664", "namespace": "debian:distro:debian:unstable", "packages": [ "shiro" ] }, { "reason": "added", "id": "CVE-2022-40664", "namespace": "nvd:cpe", "packages": [ "shiro" ] }, { "reason": "added", "id": "CVE-2022-40664", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40664", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40664", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40664", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40664", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40674", "namespace": "nvd:cpe", "packages": [ "libexpat" ] }, { "reason": "changed", "id": "CVE-2022-40674", "namespace": "redhat:distro:redhat:8", "packages": [ "expat", "firefox", "firefox:flatpak/firefox", "thunderbird", "thunderbird:flatpak/thunderbird", "xmlrpc-c" ] }, { "reason": "changed", "id": "CVE-2022-40768", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40777", "namespace": "nvd:cpe", "packages": [ "email_marketer" ] }, { "reason": "added", "id": "CVE-2022-40871", "namespace": "nvd:cpe", "packages": [ "dolibarr_erp/crm" ] }, { "reason": "added", "id": "CVE-2022-40871", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40871", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40871", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40871", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-40871", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-40921", "namespace": "nvd:cpe", "packages": [ "dedecms" ] }, { "reason": "added", "id": "CVE-2022-41031", "namespace": "nvd:cpe", "packages": [ "365_apps", "office", "office_long_term_servicing_channel" ] }, { "reason": "added", "id": "CVE-2022-41032", "namespace": "nvd:cpe", "packages": [ ".net", ".net_core", "visual_studio_2019", "visual_studio_2022" ] }, { "reason": "added", "id": "CVE-2022-41032", "namespace": "redhat:distro:redhat:8", "packages": [ "dotnet3.1", "dotnet6.0", "dotnet7.0" ] }, { "reason": "added", "id": "CVE-2022-41032", "namespace": "redhat:distro:redhat:9", "packages": [ "dotnet6.0", "dotnet7.0" ] }, { "reason": "added", "id": "CVE-2022-41034", "namespace": "nvd:cpe", "packages": [ "visual_studio_code" ] }, { "reason": "changed", "id": "CVE-2022-41035", "namespace": "nvd:cpe", "packages": [ "edge_chromium" ] }, { "reason": "added", "id": "CVE-2022-41036", "namespace": "nvd:cpe", "packages": [ "sharepoint_foundation", "sharepoint_server" ] }, { "reason": "added", "id": "CVE-2022-41037", "namespace": "nvd:cpe", "packages": [ "sharepoint_foundation", "sharepoint_server" ] }, { "reason": "added", "id": "CVE-2022-41038", "namespace": "nvd:cpe", "packages": [ "sharepoint_foundation", "sharepoint_server" ] }, { "reason": "added", "id": "CVE-2022-41042", "namespace": "nvd:cpe", "packages": [ "visual_studio_code" ] }, { "reason": "changed", "id": "CVE-2022-41043", "namespace": "nvd:cpe", "packages": [ "office", "office_long_term_servicing_channel" ] }, { "reason": "added", "id": "CVE-2022-41083", "namespace": "nvd:cpe", "packages": [ "jupyter" ] }, { "reason": "changed", "id": "CVE-2022-41166", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41167", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41168", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41169", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41170", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41171", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41172", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41173", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41174", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41175", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41176", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41177", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41178", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41179", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41180", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41181", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41182", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41183", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41184", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41185", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_author" ] }, { "reason": "changed", "id": "CVE-2022-41186", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41187", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41188", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41189", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41190", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41191", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41192", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41193", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41194", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41195", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41196", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41197", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41198", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41199", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41200", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41201", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41202", "namespace": "nvd:cpe", "packages": [ "3d_visual_enterprise_viewer" ] }, { "reason": "changed", "id": "CVE-2022-41204", "namespace": "nvd:cpe", "packages": [ "commerce" ] }, { "reason": "changed", "id": "CVE-2022-41206", "namespace": "nvd:cpe", "packages": [ "businessobjects_business_intelligence" ] }, { "reason": "changed", "id": "CVE-2022-41209", "namespace": "nvd:cpe", "packages": [ "customer_data_cloud" ] }, { "reason": "changed", "id": "CVE-2022-41210", "namespace": "nvd:cpe", "packages": [ "customer_data_cloud" ] }, { "reason": "changed", "id": "CVE-2022-41301", "namespace": "nvd:cpe", "packages": [ "subassembly_composer" ] }, { "reason": "added", "id": "CVE-2022-41302", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41303", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41304", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41305", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41306", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41307", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41308", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41316", "namespace": "alpine:distro:alpine:3.16", "packages": [ "vault" ] }, { "reason": "added", "id": "CVE-2022-41316", "namespace": "nvd:cpe", "packages": [ "vault" ] }, { "reason": "changed", "id": "CVE-2022-41323", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41323", "namespace": "debian:distro:debian:11", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-41323", "namespace": "debian:distro:debian:12", "packages": [ "python-django" ] }, { "reason": "changed", "id": "CVE-2022-41323", "namespace": "debian:distro:debian:unstable", "packages": [ "python-django" ] }, { "reason": "added", "id": "CVE-2022-41323", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41348", "namespace": "nvd:cpe", "packages": [ "collaboration" ] }, { "reason": "added", "id": "CVE-2022-41349", "namespace": "nvd:cpe", "packages": [ "collaboration" ] }, { "reason": "added", "id": "CVE-2022-41350", "namespace": "nvd:cpe", "packages": [ "collaboration" ] }, { "reason": "added", "id": "CVE-2022-41351", "namespace": "nvd:cpe", "packages": [ "collaboration" ] }, { "reason": "changed", "id": "CVE-2022-41380", "namespace": "nvd:cpe", "packages": [ "d8s-yaml" ] }, { "reason": "changed", "id": "CVE-2022-41381", "namespace": "nvd:cpe", "packages": [ "d8s-utility" ] }, { "reason": "changed", "id": "CVE-2022-41382", "namespace": "nvd:cpe", "packages": [ "d8s-json" ] }, { "reason": "changed", "id": "CVE-2022-41383", "namespace": "nvd:cpe", "packages": [ "d8s-archives" ] }, { "reason": "changed", "id": "CVE-2022-41384", "namespace": "nvd:cpe", "packages": [ "d8s-domains" ] }, { "reason": "changed", "id": "CVE-2022-41385", "namespace": "nvd:cpe", "packages": [ "d8s-html" ] }, { "reason": "changed", "id": "CVE-2022-41386", "namespace": "nvd:cpe", "packages": [ "d8s-utility" ] }, { "reason": "changed", "id": "CVE-2022-41387", "namespace": "nvd:cpe", "packages": [ "d8s-pdfs" ] }, { "reason": "added", "id": "CVE-2022-41390", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41391", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41392", "namespace": "nvd:cpe", "packages": [ "total.js" ] }, { "reason": "added", "id": "CVE-2022-41403", "namespace": "nvd:cpe", "packages": [ "newsletter_subscribe_(popup_+_regular_module)" ] }, { "reason": "changed", "id": "CVE-2022-41404", "namespace": "debian:distro:debian:10", "packages": [ "ini4j" ] }, { "reason": "changed", "id": "CVE-2022-41404", "namespace": "debian:distro:debian:11", "packages": [ "ini4j" ] }, { "reason": "changed", "id": "CVE-2022-41404", "namespace": "debian:distro:debian:12", "packages": [ "ini4j" ] }, { "reason": "changed", "id": "CVE-2022-41404", "namespace": "debian:distro:debian:unstable", "packages": [ "ini4j" ] }, { "reason": "changed", "id": "CVE-2022-41404", "namespace": "nvd:cpe", "packages": [ "ini4j" ] }, { "reason": "added", "id": "CVE-2022-41404", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41404", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41404", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41404", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41404", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41406", "namespace": "nvd:cpe", "packages": [ "church_management_system" ] }, { "reason": "changed", "id": "CVE-2022-41407", "namespace": "nvd:cpe", "packages": [ "online_pet_shop_we_app" ] }, { "reason": "changed", "id": "CVE-2022-41408", "namespace": "nvd:cpe", "packages": [ "online_pet_shop_we_app" ] }, { "reason": "added", "id": "CVE-2022-41416", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41436", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41473", "namespace": "nvd:cpe", "packages": [ "rpcms" ] }, { "reason": "added", "id": "CVE-2022-41474", "namespace": "nvd:cpe", "packages": [ "rpcms" ] }, { "reason": "added", "id": "CVE-2022-41475", "namespace": "nvd:cpe", "packages": [ "rpcms" ] }, { "reason": "added", "id": "CVE-2022-41477", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41480", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41481", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41482", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41483", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41484", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41485", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41489", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41495", "namespace": "nvd:cpe", "packages": [ "clippercms" ] }, { "reason": "added", "id": "CVE-2022-41496", "namespace": "nvd:cpe", "packages": [ "icms" ] }, { "reason": "added", "id": "CVE-2022-41497", "namespace": "nvd:cpe", "packages": [ "clippercms" ] }, { "reason": "changed", "id": "CVE-2022-41530", "namespace": "nvd:cpe", "packages": [ "open_source_sacco_management_system" ] }, { "reason": "changed", "id": "CVE-2022-41532", "namespace": "nvd:cpe", "packages": [ "open_source_sacco_management_system" ] }, { "reason": "added", "id": "CVE-2022-41533", "namespace": "nvd:cpe", "packages": [ "online_diagnostic_lab_management_system" ] }, { "reason": "added", "id": "CVE-2022-41534", "namespace": "nvd:cpe", "packages": [ "online_diagnostic_lab_management_system" ] }, { "reason": "added", "id": "CVE-2022-41535", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41536", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41538", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41539", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41550", "namespace": "debian:distro:debian:10", "packages": [ "libosip2" ] }, { "reason": "changed", "id": "CVE-2022-41550", "namespace": "debian:distro:debian:11", "packages": [ "libosip2" ] }, { "reason": "changed", "id": "CVE-2022-41550", "namespace": "debian:distro:debian:12", "packages": [ "libosip2" ] }, { "reason": "changed", "id": "CVE-2022-41550", "namespace": "debian:distro:debian:unstable", "packages": [ "libosip2" ] }, { "reason": "changed", "id": "CVE-2022-41550", "namespace": "nvd:cpe", "packages": [ "osip" ] }, { "reason": "added", "id": "CVE-2022-41550", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41550", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41550", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41550", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41550", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41576", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41577", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41578", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41580", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41581", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41582", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41583", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41584", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41585", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41586", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41587", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41588", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41589", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41592", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41593", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41594", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41595", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41597", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41598", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41600", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41601", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41602", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41603", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41606", "namespace": "debian:distro:debian:11", "packages": [ "nomad" ] }, { "reason": "changed", "id": "CVE-2022-41606", "namespace": "debian:distro:debian:unstable", "packages": [ "nomad" ] }, { "reason": "changed", "id": "CVE-2022-41606", "namespace": "nvd:cpe", "packages": [ "nomad" ] }, { "reason": "added", "id": "CVE-2022-41606", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41606", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41606", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41606", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41606", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41623", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41665", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "alpine:distro:alpine:3.15", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "alpine:distro:alpine:3.16", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "alpine:distro:alpine:edge", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41674", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-41686", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-41715", "namespace": "debian:distro:debian:10", "packages": [ "golang-1.11" ] }, { "reason": "changed", "id": "CVE-2022-41715", "namespace": "debian:distro:debian:11", "packages": [ "golang-1.15" ] }, { "reason": "changed", "id": "CVE-2022-41715", "namespace": "debian:distro:debian:12", "packages": [ "golang-1.18", "golang-1.19" ] }, { "reason": "changed", "id": "CVE-2022-41715", "namespace": "debian:distro:debian:unstable", "packages": [ "golang-1.18", "golang-1.19" ] }, { "reason": "added", "id": "CVE-2022-41715", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-41715", "namespace": "redhat:distro:redhat:8", "packages": [ "container-tools:3.0/buildah", "container-tools:3.0/containernetworking-plugins", "container-tools:3.0/oci-seccomp-bpf-hook", "container-tools:3.0/podman", "container-tools:3.0/runc", "container-tools:3.0/skopeo", "container-tools:3.0/toolbox", "container-tools:4.0/buildah", "container-tools:4.0/containernetworking-plugins", "container-tools:4.0/oci-seccomp-bpf-hook", "container-tools:4.0/podman", "container-tools:4.0/runc", "container-tools:4.0/skopeo", "container-tools:4.0/toolbox", "container-tools:rhel8/buildah", "container-tools:rhel8/containernetworking-plugins", "container-tools:rhel8/oci-seccomp-bpf-hook", "container-tools:rhel8/podman", "container-tools:rhel8/runc", "container-tools:rhel8/skopeo", "container-tools:rhel8/toolbox", "git-lfs", "go-toolset:rhel8/golang", "grafana", "grafana-pcp", "osbuild-composer", "rsyslog", "weldr-client" ] }, { "reason": "added", "id": "CVE-2022-41715", "namespace": "redhat:distro:redhat:9", "packages": [ "buildah", "butane", "containernetworking-plugins", "git-lfs", "golang", "grafana", "grafana-pcp", "ignition", "oci-seccomp-bpf-hook", "osbuild-composer", "podman", "rsyslog", "runc", "skopeo", "weldr-client" ] }, { "reason": "changed", "id": "CVE-2022-41828", "namespace": "nvd:cpe", "packages": [ "amazon_web_services_redshift_java_database_connectivity_driver" ] }, { "reason": "changed", "id": "CVE-2022-41851", "namespace": "nvd:cpe", "packages": [ "jt_open_toolkit", "simcenter_femap" ] }, { "reason": "changed", "id": "CVE-2022-42003", "namespace": "debian:distro:debian:10", "packages": [ "jackson-databind" ] }, { "reason": "changed", "id": "CVE-2022-42003", "namespace": "debian:distro:debian:11", "packages": [ "jackson-databind" ] }, { "reason": "changed", "id": "CVE-2022-42003", "namespace": "debian:distro:debian:12", "packages": [ "jackson-databind" ] }, { "reason": "changed", "id": "CVE-2022-42003", "namespace": "debian:distro:debian:unstable", "packages": [ "jackson-databind" ] }, { "reason": "changed", "id": "CVE-2022-42003", "namespace": "nvd:cpe", "packages": [ "jackson-databind" ] }, { "reason": "changed", "id": "CVE-2022-42010", "namespace": "nvd:cpe", "packages": [ "d-bus" ] }, { "reason": "added", "id": "CVE-2022-42010", "namespace": "redhat:distro:redhat:6", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42010", "namespace": "redhat:distro:redhat:7", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42010", "namespace": "redhat:distro:redhat:8", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42010", "namespace": "redhat:distro:redhat:9", "packages": [ "dbus" ] }, { "reason": "changed", "id": "CVE-2022-42011", "namespace": "nvd:cpe", "packages": [ "d-bus" ] }, { "reason": "added", "id": "CVE-2022-42011", "namespace": "redhat:distro:redhat:6", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42011", "namespace": "redhat:distro:redhat:7", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42011", "namespace": "redhat:distro:redhat:8", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42011", "namespace": "redhat:distro:redhat:9", "packages": [ "dbus" ] }, { "reason": "changed", "id": "CVE-2022-42012", "namespace": "nvd:cpe", "packages": [ "d-bus" ] }, { "reason": "added", "id": "CVE-2022-42012", "namespace": "redhat:distro:redhat:6", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42012", "namespace": "redhat:distro:redhat:7", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42012", "namespace": "redhat:distro:redhat:8", "packages": [ "dbus" ] }, { "reason": "added", "id": "CVE-2022-42012", "namespace": "redhat:distro:redhat:9", "packages": [ "dbus" ] }, { "reason": "changed", "id": "CVE-2022-42036", "namespace": "nvd:cpe", "packages": [ "d8s-urls" ] }, { "reason": "changed", "id": "CVE-2022-42037", "namespace": "nvd:cpe", "packages": [ "d8s-asns" ] }, { "reason": "changed", "id": "CVE-2022-42038", "namespace": "nvd:cpe", "packages": [ "d8s-ip-addresses" ] }, { "reason": "changed", "id": "CVE-2022-42039", "namespace": "nvd:cpe", "packages": [ "d8s-lists" ] }, { "reason": "changed", "id": "CVE-2022-42040", "namespace": "nvd:cpe", "packages": [ "d8s-algorithms" ] }, { "reason": "changed", "id": "CVE-2022-42041", "namespace": "nvd:cpe", "packages": [ "d8s-file-system" ] }, { "reason": "changed", "id": "CVE-2022-42042", "namespace": "nvd:cpe", "packages": [ "d8s-networking" ] }, { "reason": "changed", "id": "CVE-2022-42043", "namespace": "nvd:cpe", "packages": [ "d8s-xml" ] }, { "reason": "changed", "id": "CVE-2022-42044", "namespace": "nvd:cpe", "packages": [ "d8s-asns" ] }, { "reason": "added", "id": "CVE-2022-42064", "namespace": "nvd:cpe", "packages": [ "online_diagnostic_lab_management_system" ] }, { "reason": "added", "id": "CVE-2022-42066", "namespace": "nvd:cpe", "packages": [ "online_examination_system" ] }, { "reason": "added", "id": "CVE-2022-42067", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42069", "namespace": "nvd:cpe", "packages": [ "online_birth_certificate_management_system" ] }, { "reason": "added", "id": "CVE-2022-42070", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42071", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42077", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42078", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42079", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42080", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42081", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42086", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42087", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42156", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42159", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42160", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42161", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42232", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42234", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42339", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42340", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42341", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42342", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42463", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42464", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42488", "namespace": "nvd:cpe", "packages": [] }, { "reason": "changed", "id": "CVE-2022-42711", "namespace": "nvd:cpe", "packages": [ "whatsup_gold" ] }, { "reason": "added", "id": "CVE-2022-42715", "namespace": "nvd:cpe", "packages": [ "redcap" ] }, { "reason": "changed", "id": "CVE-2022-42717", "namespace": "nvd:cpe", "packages": [ "packer" ] }, { "reason": "added", "id": "CVE-2022-42717", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42717", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42717", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42717", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42717", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "alpine:distro:alpine:3.15", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "alpine:distro:alpine:3.16", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "alpine:distro:alpine:edge", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42719", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "alpine:distro:alpine:3.15", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "alpine:distro:alpine:3.16", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "alpine:distro:alpine:edge", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42720", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "alpine:distro:alpine:3.15", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "alpine:distro:alpine:3.16", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "alpine:distro:alpine:edge", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42721", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "alpine:distro:alpine:3.15", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "alpine:distro:alpine:3.16", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "alpine:distro:alpine:edge", "packages": [ "linux-lts" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "debian:distro:debian:10", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "debian:distro:debian:11", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "debian:distro:debian:12", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "debian:distro:debian:unstable", "packages": [ "linux" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "redhat:distro:redhat:6", "packages": [ "kernel" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "redhat:distro:redhat:7", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "redhat:distro:redhat:8", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "redhat:distro:redhat:9", "packages": [ "kernel", "kernel-rt" ] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42722", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "debian:distro:debian:11", "packages": [ "commons-text" ] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "debian:distro:debian:12", "packages": [ "commons-text" ] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "debian:distro:debian:unstable", "packages": [ "commons-text" ] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42889", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42897", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42899", "namespace": "nvd:cpe", "packages": [ "microstation", "view" ] }, { "reason": "added", "id": "CVE-2022-42900", "namespace": "nvd:cpe", "packages": [ "microstation", "view" ] }, { "reason": "added", "id": "CVE-2022-42901", "namespace": "nvd:cpe", "packages": [ "microstation", "view" ] }, { "reason": "added", "id": "CVE-2022-42902", "namespace": "debian:distro:debian:10", "packages": [ "lava" ] }, { "reason": "added", "id": "CVE-2022-42902", "namespace": "debian:distro:debian:11", "packages": [ "lava" ] }, { "reason": "added", "id": "CVE-2022-42902", "namespace": "debian:distro:debian:unstable", "packages": [ "lava" ] }, { "reason": "added", "id": "CVE-2022-42902", "namespace": "nvd:cpe", "packages": [ "lava" ] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "debian:distro:debian:10", "packages": [ "powerline-gitstatus" ] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "debian:distro:debian:11", "packages": [ "powerline-gitstatus" ] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "debian:distro:debian:12", "packages": [ "powerline-gitstatus" ] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "debian:distro:debian:unstable", "packages": [ "powerline-gitstatus" ] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "ubuntu:distro:ubuntu:14.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "ubuntu:distro:ubuntu:16.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "ubuntu:distro:ubuntu:18.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "ubuntu:distro:ubuntu:20.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42906", "namespace": "ubuntu:distro:ubuntu:22.04", "packages": [] }, { "reason": "added", "id": "CVE-2022-42961", "namespace": "debian:distro:debian:11", "packages": [ "wolfssl" ] }, { "reason": "added", "id": "CVE-2022-42961", "namespace": "debian:distro:debian:12", "packages": [ "wolfssl" ] }, { "reason": "added", "id": "CVE-2022-42961", "namespace": "debian:distro:debian:unstable", "packages": [ "wolfssl" ] }, { "reason": "added", "id": "CVE-2022-42961", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42968", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "CVE-2022-42969", "namespace": "nvd:cpe", "packages": [] }, { "reason": "added", "id": "ELSA-2022-6911", "namespace": "oracle:distro:oraclelinux:8", "packages": [ "aspnetcore-runtime-6.0", "aspnetcore-targeting-pack-6.0", "dotnet", "dotnet-apphost-pack-6.0", "dotnet-host", "dotnet-hostfxr-6.0", "dotnet-runtime-6.0", "dotnet-sdk-6.0", "dotnet-sdk-6.0-source-built-artifacts", "dotnet-targeting-pack-6.0", "dotnet-templates-6.0", "netstandard-targeting-pack-2.1" ] }, { "reason": "added", "id": "ELSA-2022-9862", "namespace": "oracle:distro:oraclelinux:8", "packages": [ "hivex", "hivex-devel", "libguestfs", "libguestfs-appliance", "libguestfs-bash-completion", "libguestfs-devel", "libguestfs-gfs2", "libguestfs-gobject", "libguestfs-gobject-devel", "libguestfs-inspect-icons", "libguestfs-java", "libguestfs-java-devel", "libguestfs-javadoc", "libguestfs-man-pages-ja", "libguestfs-man-pages-uk", "libguestfs-rescue", "libguestfs-rsync", "libguestfs-tools", "libguestfs-tools-c", "libguestfs-winsupport", "libguestfs-xfs", "libiscsi", "libiscsi-devel", "libiscsi-utils", "libnbd", "libnbd-bash-completion", "libnbd-devel", "libtpms", "libtpms-devel", "libvirt", "libvirt-client", "libvirt-daemon", "libvirt-daemon-config-network", "libvirt-daemon-config-nwfilter", "libvirt-daemon-driver-interface", "libvirt-daemon-driver-network", "libvirt-daemon-driver-nodedev", "libvirt-daemon-driver-nwfilter", "libvirt-daemon-driver-qemu", "libvirt-daemon-driver-secret", "libvirt-daemon-driver-storage", "libvirt-daemon-driver-storage-core", "libvirt-daemon-driver-storage-disk", "libvirt-daemon-driver-storage-gluster", "libvirt-daemon-driver-storage-iscsi", "libvirt-daemon-driver-storage-iscsi-direct", "libvirt-daemon-driver-storage-logical", "libvirt-daemon-driver-storage-mpath", "libvirt-daemon-driver-storage-rbd", "libvirt-daemon-driver-storage-scsi", "libvirt-daemon-kvm", "libvirt-dbus", "libvirt-devel", "libvirt-docs", "libvirt-libs", "libvirt-lock-sanlock", "libvirt-nss", "libvirt-wireshark", "lua-guestfs", "nbdfuse", "nbdkit", "nbdkit-bash-completion", "nbdkit-basic-filters", "nbdkit-basic-plugins", "nbdkit-curl-plugin", "nbdkit-devel", "nbdkit-example-plugins", "nbdkit-gzip-filter", "nbdkit-gzip-plugin", "nbdkit-linuxdisk-plugin", "nbdkit-nbd-plugin", "nbdkit-python-plugin", "nbdkit-server", "nbdkit-ssh-plugin", "nbdkit-tar-filter", "nbdkit-tar-plugin", "nbdkit-tmpdisk-plugin", "nbdkit-vddk-plugin", "nbdkit-xz-filter", "netcf", "netcf-devel", "netcf-libs", "perl-hivex", "perl-sys-guestfs", "perl-sys-virt", "python3-hivex", "python3-libguestfs", "python3-libnbd", "python3-libvirt", "qemu-guest-agent", "qemu-img", "qemu-kvm", "qemu-kvm-block-curl", "qemu-kvm-block-gluster", "qemu-kvm-block-iscsi", "qemu-kvm-block-rbd", "qemu-kvm-block-ssh", "qemu-kvm-common", "qemu-kvm-core", "qemu-virtiofsd", "ruby-hivex", "ruby-libguestfs", "seabios", "seabios-bin", "seavgabios-bin", "sgabios", "sgabios-bin", "supermin", "supermin-devel", "swtpm", "swtpm-devel", "swtpm-libs", "swtpm-tools", "swtpm-tools-pkcs11", "virt-dib", "virt-v2v", "virt-v2v-bash-completion", "virt-v2v-man-pages-ja", "virt-v2v-man-pages-uk" ] }, { "reason": "changed", "id": "GHSA-3fhf-6939-qg8p", "namespace": "github:language:ruby", "packages": [ "rest-client" ] }, { "reason": "changed", "id": "GHSA-45x9-q6vj-cqgq", "namespace": "github:language:java", "packages": [ "org.apache.shiro:shiro-core" ] }, { "reason": "changed", "id": "GHSA-4f63-89w9-3jjv", "namespace": "github:language:rust", "packages": [ "openssl-src" ] }, { "reason": "added", "id": "GHSA-599f-7c49-w659", "namespace": "github:language:java", "packages": [ "org.apache.commons:commons-text" ] }, { "reason": "changed", "id": "GHSA-5c6q-f783-h888", "namespace": "github:language:java", "packages": [ "com.amazon.redshift:redshift-jdbc42" ] }, { "reason": "changed", "id": "GHSA-7v3g-4878-5qrf", "namespace": "github:language:go", "packages": [ "github.com/hashicorp/nomad" ] }, { "reason": "added", "id": "GHSA-824x-jcxf-hpfg", "namespace": "github:language:python", "packages": [ "rdiffweb" ] }, { "reason": "added", "id": "GHSA-92gf-p376-6r9r", "namespace": "github:language:python", "packages": [ "rdiffweb" ] }, { "reason": "added", "id": "GHSA-qj6r-fhrc-jj5r", "namespace": "github:language:go", "packages": [ "github.com/hyperledger/fabric" ] }, { "reason": "added", "id": "GHSA-w67g-6gjv-c599", "namespace": "github:language:python", "packages": [ "powerline-gitstatus" ] }, { "reason": "added", "id": "GHSA-x4q7-m6fp-4v9v", "namespace": "github:language:php", "packages": [ "october/system" ] }, { "reason": "added", "id": "GHSA-x8x2-wc2h-wc48", "namespace": "github:language:python", "packages": [ "rdiffweb" ] } ] ================================================ FILE: test/integration/testdata/vex/csaf/affected.csaf.json ================================================ { "document": { "category": "csaf_vex", "csaf_version": "2.0", "notes": [ { "category": "summary", "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", "title": "Author comment" } ], "publisher": { "category": "vendor", "name": "Example Company ProductCERT", "namespace": "https://psirt.example.com" }, "title": "Example VEX Document", "tracking": { "current_release_date": "2024-04-25T11:00:00.000Z", "generator": { "date": "2024-04-25T11:00:00.000Z", "engine": { "name": "Secvisogram", "version": "1.11.0" } }, "id": "2022-EVD-UC-01-A-001", "initial_release_date": "2024-04-25T11:00:00.000Z", "revision_history": [ { "date": "2024-04-25T11:00:00.000Z", "number": "1", "summary": "Initial version." } ], "status": "final", "version": "1" } }, "product_tree": { "branches": [ { "branches": [ { "branches": [ { "category": "product_version", "name": "0.9.9", "product": { "name": "LibVNCServer 0.9.9", "product_id": "CSAFPID-0001", "product_identification_helper": { "purl": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" } } } ], "category": "product_name", "name": "LibVNCServer" } ], "category": "vendor", "name": "Example Company" } ] }, "vulnerabilities": [ { "cve": "CVE-2024-0000", "notes": [ { "category": "description", "text": "A CVE affecting libvncserver.", "title": "CVE description" } ], "product_status": { "known_affected": [ "CSAFPID-0001" ] }, "remediations": [ { "category": "vendor_fix", "details": "Customers should update to version 1.1 of product DEF which fixes the issue.", "product_ids": [ "CSAFPID-0001" ] } ] } ] } ================================================ FILE: test/integration/testdata/vex/csaf/under_investigation.csaf.json ================================================ { "document": { "category": "csaf_vex", "csaf_version": "2.0", "notes": [ { "category": "summary", "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", "title": "Author comment" } ], "publisher": { "category": "vendor", "name": "Example Company ProductCERT", "namespace": "https://psirt.example.com" }, "title": "Example VEX Document", "tracking": { "current_release_date": "2024-04-25T11:00:00.000Z", "generator": { "date": "2024-04-25T11:00:00.000Z", "engine": { "name": "Secvisogram", "version": "1.11.0" } }, "id": "2022-EVD-UC-01-A-001", "initial_release_date": "2024-04-25T11:00:00.000Z", "revision_history": [ { "date": "2024-04-25T11:00:00.000Z", "number": "1", "summary": "Initial version." } ], "status": "final", "version": "1" } }, "product_tree": { "branches": [ { "branches": [ { "branches": [ { "category": "product_version", "name": "0.9.9", "product": { "name": "LibVNCServer 0.9.9", "product_id": "CSAFPID-0001", "product_identification_helper": { "purl": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" } } } ], "category": "product_name", "name": "LibVNCServer" } ], "category": "vendor", "name": "Example Company" } ] }, "vulnerabilities": [ { "cve": "CVE-2024-0000", "notes": [ { "category": "description", "text": "A CVE affecting libvncserver.", "title": "CVE description" } ], "product_status": { "under_investigation": [ "CSAFPID-0001" ] } } ] } ================================================ FILE: test/integration/testdata/vex/openvex/affected.openvex.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", "author": "The OpenVEX Project ", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "vulnerability": { "name": "CVE-2024-0000" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "subcomponents": [ { "@id": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" } ] } ], "status": "affected" } ] } ================================================ FILE: test/integration/testdata/vex/openvex/under_investigation.openvex.json ================================================ { "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78", "author": "The OpenVEX Project ", "timestamp": "2023-07-17T18:28:47.696004345-06:00", "version": 1, "statements": [ { "timestamp": "2023-07-16T18:28:47.696004345-06:00", "vulnerability": { "name": "CVE-2024-0000" }, "products": [ { "@id": "pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "subcomponents": [ { "@id": "pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0" } ] } ], "status": "under_investigation" } ] } ================================================ FILE: test/integration/utils_test.go ================================================ package integration import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) const cacheDirRelativePath string = "./testdata/cache" func PullThroughImageCache(t testing.TB, imageName string) string { cacheDirectory, absErr := filepath.Abs(cacheDirRelativePath) if absErr != nil { t.Fatalf("could not get absolute path of cache directory %s; %v", cacheDirRelativePath, absErr) } mkdirError := os.MkdirAll(cacheDirectory, 0755) if mkdirError != nil { t.Fatalf("could not create cache directory %s; %v", cacheDirRelativePath, absErr) } re := regexp.MustCompile("[/:]") archiveFileName := fmt.Sprintf("%s.tar", re.ReplaceAllString(imageName, "-")) imageArchivePath := filepath.Join(cacheDirectory, archiveFileName) if _, err := os.Stat(imageArchivePath); os.IsNotExist(err) { t.Logf("Cache miss for image %s; copying to archive at %s", imageName, imageArchivePath) saveImage(t, imageName, imageArchivePath) } return imageArchivePath } func saveImage(t testing.TB, imageName string, destPath string) { sourceImage := fmt.Sprintf("docker://docker.io/%s", imageName) destinationString := fmt.Sprintf("docker-archive:%s", destPath) skopeoPath := filepath.Join(repoRoot(t), ".tool", "skopeo") policyPath := filepath.Join(repoRoot(t), "test", "integration", "testdata", "skopeo-policy.json") skopeoCommand := []string{ "--policy", policyPath, "copy", "--override-os", "linux", sourceImage, destinationString, } cmd := exec.Command(skopeoPath, skopeoCommand...) out, err := cmd.Output() if err != nil { var exitError *exec.ExitError if errors.As(err, &exitError) { t.Logf("Stderr: %s", exitError.Stderr) } t.Fatal(err) } t.Logf("Stdout: %s\n", out) } func getSyftSBOM(t testing.TB, image, from string, encoder sbom.FormatEncoder) string { src, err := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources(from)) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, src.Close()) }) config := syft.DefaultCreateSBOMConfig() config.Search.Scope = source.SquashedScope // TODO: relationships are not verified at this time s, err := syft.CreateSBOM(context.Background(), src, config) require.NoError(t, err) require.NotNil(t, s) var buf bytes.Buffer err = encoder.Encode(&buf, *s) require.NoError(t, err) return buf.String() } func getMatchSet(matches match.Matches) *strset.Set { s := strset.New() for _, m := range matches.Sorted() { s.Add(fmt.Sprintf("%s-%s-%s", m.Vulnerability.ID, m.Package.Name, m.Package.Version)) } return s } func repoRoot(tb testing.TB) string { tb.Helper() root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { tb.Fatalf("unable to find repo root dir: %+v", err) } absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) if err != nil { tb.Fatal("unable to get abs path to repo root:", err) } return absRepoRoot } ================================================ FILE: test/quality/.gitignore ================================================ venv .yardstick/tools .yardstick/result stage pull migrate.py .oras-cache *.tar.gz *.tar.zst ================================================ FILE: test/quality/.grype.yaml ================================================ by-cve: true # we want to be able to validate CPE findings with the broadest lens possible (not just the default configuration) # to aid in validating CPE related changes (whereas the default configuration is more focused on non CPE matching) match: java: using-cpes: true jvm: using-cpes: true dotnet: using-cpes: true golang: using-cpes: true always-use-cpe-for-stdlib: true javascript: using-cpes: true python: using-cpes: true ruby: using-cpes: true rust: using-cpes: true stock: using-cpes: true ================================================ FILE: test/quality/.python-version ================================================ 3.10.7 ================================================ FILE: test/quality/.yardstick.yaml ================================================ x-ref: # note: always reference images with BOTH a tag and a digest images: &images - docker.io/cloudbees/cloudbees-core-agent:2.289.2.2@sha256:d48f0546b4cf5ef4626136242ce302f94a42751156b7be42f4b1b75a66608880 - docker.io/cloudbees/cloudbees-core-mm:2.277.3.1@sha256:4c564f473d38f23da1caa48c4ef53b958ef03d279232007ad3319b1f38584bdb - docker.io/cloudbees/cloudbees-core-oc:2.289.2.2@sha256:9cd85ee84e401dc27e3a8268aae67b594a651b2f4c7fc056ca14c7b0a0a6b82d - docker.io/anchore/test_images:grype-quality-dotnet-69f15d2@sha256:e25a9a175433c2bfe9c04e6482e6c5eca0491629144c78061763f7f604fdea80 - docker.io/anchore/test_images:grype-quality-node-d89207b@sha256:f56164678054e5eb59ab838367373a49df723b324617b1ba6de775749d7f91d4 - docker.io/anchore/test_images:grype-quality-python-d89207b@sha256:b2b58a55c0b03c1626d2aaae2add9832208b02124dda7b7b41811e14f0fb272c - docker.io/anchore/test_images:grype-quality-java-d89207b@sha256:b3534fc2e37943136d5b54e3a58b55d4ccd4363d926cf7aa5bf55a524cf8275b - docker.io/anchore/test_images:grype-quality-golang-d89207b@sha256:7536ee345532f674ec9e448e3768db4e546c48220ba2b6ec9bc9cfbfb3b7b74a - docker.io/anchore/test_images:grype-quality-ruby-d89207b@sha256:1a5a5f870924e88a6f0f2b8089cf276ef0a79b5244a052cdfe4a47bb9e5a2c10 - docker.io/anchore/test_images:vulnerabilities-package-name-normalization@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199 - docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 - docker.io/anchore/test_images:java-56d52bc@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da - docker.io/anchore/test_images:npm-56d52bc@sha256:ba42ded8613fc643d407a050faf5ab48cfb405ad3ef2015bf6feeb5dff44738d - docker.io/anchore/test_images:gems-56d52bc@sha256:5763c8a225f950961bf01ddec68e36f18e236130e182f2b9290a6e03b9777bfe - docker.io/anchore/test_images:golang-56d52bc@sha256:d1819e59e89e8ea90073460acb4ebb2ee18ccead9fa880dae91e8fc61b19ca1c - docker.io/anchore/test_images:ubuntu-content-56d52bc@sha256:f8e72da9f67caf90714926e7b21f0da93ca1e528b37a97dffe71e2ec38872a8b - docker.io/anchore/test_images:vulnerabilities-alpine-3.11-d5be50d@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6 - docker.io/anchore/test_images:vulnerabilities-alpine-3.12-d5be50d@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1 - docker.io/anchore/test_images:vulnerabilities-alpine-3.13-d5be50d@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798 - docker.io/anchore/test_images:vulnerabilities-alpine-3.14-d5be50d@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5 - docker.io/anchore/test_images:vulnerabilities-alpine-3.15-d5be50d@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653 - docker.io/anchore/test_images:vulnerabilities-alpine-3.6-d5be50d@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d - docker.io/anchore/test_images:vulnerabilities-alpine-3.8-d5be50d@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb - docker.io/anchore/test_images:vulnerabilities-amazonlinux-2-5c26ce9@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa - docker.io/anchore/test_images:vulnerabilities-centos@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f - docker.io/anchore/test_images:vulnerabilities-no-distro-6bde59e@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1 - docker.io/anchore/test_images:syft_bin-cf22714@sha256:c27b02c6322180fd8a7a3097d2b430bfdf9ea52ecf136edf258458e82f2c6f21 - docker.io/anchore/test_images:alpine-package-cpe-vuln-match-bd0aaef@sha256:0825acea611c7c5cc792bc7cc20de44d7413fd287dc5afc4aab9c1891d037b4f - docker.io/alpine:3.2@sha256:ddac200f3ebc9902fb8cfcd599f41feb2151f1118929da21bcef57dc276975f9 - docker.io/centos:6@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf - docker.io/almalinux:8@sha256:cd49d7250ed7bb194d502d8a3e50bd775055ca275d1d9c2785aea72b890afe6a - docker.io/rockylinux:8@sha256:72afc2e1a20c9ddf56a81c51148ebcbe927c0a879849efe813bee77d69df1dd8 - docker.io/oraclelinux:6@sha256:a06327c0f1d18d753f2a60bb17864c84a850bb6dcbcf5946dd1a8123f6e75495 - docker.io/debian:7@sha256:81e88820a7759038ffa61cff59dfcc12d3772c3a2e75b7cfe963c952da2ad264 - docker.io/busybox:1.28.1@sha256:2107a35b58593c58ec5f4e8f2c4a70d195321078aebfadfbfb223a2ff4a4ed21 - docker.io/amazonlinux:2@sha256:1301cc9f889f21dc45733df9e58034ac1c318202b4b0f0a08d88b3fdc03004de - registry.access.redhat.com/ubi8@sha256:68fecea0d255ee253acbf0c860eaebb7017ef5ef007c25bee9eeffd29ce85b29 - docker.io/python:3.8.0-slim@sha256:5e96e03a493a54904aa8be573fc0414431afb4f47ac58fbffd03b2a725005364 - docker.io/ghost:5.2.4@sha256:42137b9bd1faf4cdea5933279c48a912d010ef614551aeb0e44308600aa3e69f # commented out lines in this list are Docker v1 images, which no longer work after docker daemon dropped support # - #docker.io/node:4.2.1-slim@sha256:af31633b87d0dc58c306b04ad9f6ca88104626363c5c085e9962832628eb09ce - docker.io/elastic/kibana:8.5.0@sha256:b9e3e52f61e0a347e38eabe80ba0859f859023bc0cc8836410320aa7eb5d3e02 - docker.io/jenkins/jenkins:2.361.4-lts-jdk11@sha256:6fd5699ab182b5d23d0e3936de6047edc30955a3a92e01c392d5a2fd583efac0 - docker.io/neo4j:4.4.14-community@sha256:fcfcbb026e0e538bf66f5fe5c4b2db3dd4931c3aae07f13a5a8c10e979596256 - docker.io/sonatype/nexus3:3.30.0@sha256:e8fea6b4279f2b5b24b36170459cb7aa3d6afe999f9d3e3713541be28bae8ec4 - cgr.dev/chainguard/wolfi-base:latest-20221001@sha256:be3834598c3c4b76ace6a866edcbbe1fa18086f9ee238b57769e4d230cd7d507 - docker.io/gitlab/gitlab-ce:15.6.1-ce.0@sha256:04d4219d5dfb3acccc9997e50477c8d24b371387a95857e1ea8fc779e17a716c - docker.io/postgres:13.2@sha256:1a67ab960138c479d66834cd6bcb5b5582c53869e6052dbf4ff48d4a94c13da3 - ghcr.io/chainguard-images/scanner-test@sha256:59bddc101fba0c45d5c093575c6bc5bfee7f0e46ff127e6bb4e5acaaafb525f9 - docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50 - docker.io/datawire/aes:3.6.0@sha256:86a072278135462b6cbef70e89894df8f9b20f428b361fda2132fbb442ef257b - ghcr.io/anchore/test-images/bitnami/spark:3.2.4-debian-11-r8@sha256:267d5a6345636710b4b57b7fe981c9760203e7e092c705416310ea30a9806d74 - docker.io/grafana/grafana:9.2.4@sha256:a11c6829cdfe7fd791e48ba5b511f3562384361fb4c568ec2d8a5041ac52babe - docker.io/hashicorp/vault:1.12.0@sha256:09354ca0891f7cee8fbfe8db08c62d2d757fad8ae6c91f2b6cce7a34440e3fae - docker.io/ubuntu:12.04@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005 # - #docker.io/ubuntu:12.10@sha256:002fba3e3255af10be97ea26e476692a7ebed0bb074a9ab960b2e7a1526b15d7 # - #docker.io/ubuntu:13.04@sha256:bc48dd7075ce920ebbaa4581d3200e9fb3aaec31591061d7e3a280a04ef0248c - docker.io/ubuntu:14.04@sha256:881afbae521c910f764f7187dbfbca3cc10c26f8bafa458c76dda009a901c29d # - #docker.io/ubuntu:14.10@sha256:6341c688b4b0b82ec735389b3c97df8cf2831b8cb8bd1856779130a86574ac5c # - #docker.io/ubuntu:15.04@sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f - docker.io/ubuntu:15.10@sha256:02521a2d079595241c6793b2044f02eecf294034f31d6e235ac4b2b54ffc41f3 - docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a - docker.io/ubuntu:17.04@sha256:213e05583a7cb8756a3f998e6dd65204ddb6b4c128e2175dcdf174cdf1877459 - docker.io/ubuntu:17.10@sha256:9c4bf7dbb981591d4a1169138471afe4bf5ff5418841d00e30a7ba372e38d6c1 - docker.io/ubuntu:18.04@sha256:971a12d7e92a23183dead8bfc415aa650e7deb1cc5fed11a3d21f759a891fde9 - docker.io/ubuntu:18.10@sha256:c95b7b93ccd48c3bfd97f8cac6d5ca8053ced584c9e8e6431861ca30b0d73114 - docker.io/ubuntu:19.04@sha256:3db17bfc30b41cc18552578f4a66d7010050eb9fdc42bf6c3d82bb0dcdf88d58 - docker.io/ubuntu:19.10@sha256:6852f9e05c5bce8aa77173fa83ce611f69f271ee3a16503c5f80c199969fd1eb - docker.io/ubuntu:20.04@sha256:9d42d0e3e57bc067d10a75ee33bdd1a5298e95e5fc3c5d1fce98b455cb879249 - docker.io/ubuntu:20.10@sha256:754eb641a1ba98a8b483c3595a14164fa4ed7f4b457e1aa05f13ce06f8151723 - docker.io/ubuntu:21.04@sha256:cb92f03e258f965442b883f5402b310dd7a5ea0a661a865ad02a42bc21234bf7 - docker.io/ubuntu:21.10@sha256:253908b2844746ab3f3a08fc8a44b9b9fc1efc408d5969b093ab9ffa11eb1894 - docker.io/ubuntu:22.04@sha256:aa6c2c047467afc828e77e306041b7fa4a65734fe3449a54aa9c280822b0d87d - docker.io/ubuntu:22.10@sha256:80fb4ea0c0a384a3072a6be1879c342bb636b0d105209535ba893ba75ab38ede - docker.io/ubuntu:23.04@sha256:09f035f46361d193ded647342903b413d57d05cc06acff8285f9dda9f2d269d5 - gcr.io/distroless/python3-debian11@sha256:69ae7f133d33faab720af28e78fb45707b623bcbc94ae02a07c633bf053f4b40 - registry.suse.com/suse/sles12sp4:26.380@sha256:94b537f5b312e7397b5d0bbb3d892f961acdd9454950fc233d77f771e25335fb - registry.suse.com/suse/sle15:15.1.6.2.461@sha256:6e613c994c3b33224e439ef8ee9003fb69416f77f7a6b1da0b18981d5aa3bb75 # new vulnerabilities are added all of the time, instead of keeping up it's easier to ignore newer entries. # This approach helps tremendously with keeping the analysis relatively stable. default_max_year: 2021 result-sets: pr_vs_latest_via_sbom: description: "latest released grype vs grype from the current build (via SBOM ingestion)" validations: - max-f1-regression: 0.0 max-new-false-negatives: 00 max-unlabeled-percent: 10 max_year: 2021 matrix: images: *images tools: - name: syft # note: we want to use a fixed version of syft for capturing all results (NOT "latest") version: v1.14.0 produces: SBOM refresh: false - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. # version: git:current-commit+import-db=db.tar.zst # for local build of grype, use for example: version: path:../../+import-db=db.tar.zst takes: SBOM label: candidate - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. version: latest+import-db=db.tar.zst takes: SBOM label: reference pr_vs_latest_via_sbom_2022: description: "same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2022 and before, instead of 2021 and before" max_year: 2022 validations: - max-f1-regression: 0.0 max-new-false-negatives: 00 max-unlabeled-percent: 10 max_year: 2022 fail_on_empty_match_set: false matrix: images: - mcr.microsoft.com/cbl-mariner/base/core:2.0.20220731-arm64@sha256:51101e635f56032d5afd3fb56d66c7b93b34d5a39ddac01695d62b94473cc34e - docker.io/anchore/test_images:azurelinux3-63671fe@sha256:2d761ba36575ddd4e07d446f4f2a05448298c20e5bdcd3dedfbbc00f9865240d - docker.io/anchore/test_images:appstreams-oraclelinux-8-1a287dd@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1 - docker.io/anchore/test_images:appstreams-rhel-8-1a287dd@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b tools: - name: syft # note: we want to use a fixed version of syft for capturing all results (NOT "latest") version: v1.14.0 produces: SBOM refresh: false - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. # version: git:current-commit+import-db=db.tar.zst # for local build of grype, use for example: version: path:../../+import-db=db.tar.zst takes: SBOM label: candidate # is candidate better than the current baseline? - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. version: latest+import-db=db.tar.zst takes: SBOM label: reference # this run is the current baseline pr_vs_latest_via_sbom_2023: description: "same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2023 and before, instead of 2021 and before" max_year: 2023 validations: - max-f1-regression: 0.0 max-new-false-negatives: 00 max-unlabeled-percent: 10 max_year: 2023 fail_on_empty_match_set: false matrix: images: - docker.io/anchore/test_images:archlinux-28cca4e@sha256:a933b27534e5c911e2c660f7090aa497dee763fbbcb214a37207c2320cfedd98 - docker.io/anchore/test_images:almalinux8-271722c@sha256:6485db654df0452bd15ea71ec43e808bc8eb05b91f1c2754669a5573479a6c19 tools: - name: syft # note: we want to use a fixed version of syft for capturing all results (NOT "latest") version: v1.14.0 produces: SBOM refresh: false - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. # version: git:current-commit+import-db=db.tar.zst # for local build of grype, use for example: version: path:../../+import-db=db.tar.zst takes: SBOM label: candidate # is candidate better than the current baseline? - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. version: latest+import-db=db.tar.zst takes: SBOM label: reference # this run is the current baseline pr_vs_latest_via_sbom_2024: description: "same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2024 and before, instead of 2021 and before" max_year: 2024 validations: - max-f1-regression: 0.0 max-new-false-negatives: 00 max-unlabeled-percent: 10 max_year: 2024 fail_on_empty_match_set: false matrix: images: - ghcr.io/anchore/test-images/bitnami/redis:7.4.0@sha256:4bad45268adfdbb0b456d6bf74ded449ef79f3706cb4e473516a0a5b393968c0 # echo - ghcr.io/buildecho/scanner-test:latest@sha256:60557350ad6976dad3b88d891de8f090b20b3271c660272d30d44b5d07b23edc # minimos - docker.io/dimastopelmini/forgrype:3.1.6@sha256:ebe0c6ca122deef072c29be2f915130e5c8b4c277ad5ef551385f6496dae4dfa - docker.io/dimastopelmini/forgrype:3.1.7@sha256:653c8980c63a9ac403a3b9f56a08f43f929432ece69894423c165b4d61d3dcdb # postmarketos - ghcr.io/anchore/test-images/postmarketos:edge@sha256:2bdab220693cecfe3474055076bcbfe9ec8faf466867a5db3e0b76afaa9f4b89 - ghcr.io/anchore/test-images/postmarketos:24.06@sha256:05b42fdb332f8a5794c9d1e6ab83cd32030bf0cd3ef797ada5546419e9ad293d # Anchore test images - docker.io/anchore/test_images:appstreams-nodejs-18-rhel-9-1b0b1b4@sha256:08dbfad2d6af9afe47f7647b0b8f38fd29fc9e89306cfc39c9509981f9388b7f - docker.io/anchore/test_images:appstreams-nodejs-base-rhel-9-1b0b1b4@sha256:fc6f7a37d7e320f6ff3643d4ec9a208adb1462cd16027f045b56563e12bb0461 tools: - name: syft # note: we want to use a fixed version of syft for capturing all results (NOT "latest") version: v1.14.0 produces: SBOM refresh: false - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. # version: git:current-commit+import-db=db.tar.zst # for local build of grype, use for example: version: path:../../+import-db=db.tar.zst takes: SBOM label: candidate # is candidate better than the current baseline? - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. version: latest+import-db=db.tar.zst takes: SBOM label: reference # this run is the current baseline pr_vs_latest_via_sbom_2025: description: "same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2025 and before, instead of 2021 and before" max_year: 2025 validations: - max-f1-regression: 0.0 max-new-false-negatives: 00 max-unlabeled-percent: 10 max_year: 2025 fail_on_empty_match_set: false matrix: images: - ghcr.io/chainguard-images/scanner-test:python-library-aiohttp-chainguard@sha256:046b2c1b2df1e496eb3abf9e21224ffd879d92abc2bd57401456764d9aa8ddf1 # secureos - registry.replicated.com/library/grype-test:20250106@sha256:3339bcd874d21fa3ca5bd20636e793c0c33bd71ace3a18a9a3b3d147b91dd000 tools: - name: syft # note: we want to use a fixed version of syft for capturing all results (NOT "latest") version: v1.14.0 produces: SBOM refresh: false - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. # version: git:current-commit+import-db=db.tar.zst # for local build of grype, use for example: version: path:../../+import-db=db.tar.zst takes: SBOM label: candidate # is candidate better than the current baseline? - name: grype # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The # point of this test is to ensure the correctness of the logic in grype itself with real production data. # By pinning the DB the grype code itself becomes the independent variable under test (and not the # every-changing DB). That being said, we should be updating this DB periodically to ensure what we # are testing with is not too stale. version: latest+import-db=db.tar.zst takes: SBOM label: reference # this run is the current baseline ================================================ FILE: test/quality/Makefile ================================================ SBOM_STORE_TAG = md5-$(shell md5sum .yardstick.yaml | cut -d' ' -f1) SBOM_STORE_IMAGE = ghcr.io/anchore/grype/quality-test-sbom-store:$(SBOM_STORE_TAG) ACTIVATE_VENV = . venv/bin/activate && YARDSTICK = $(ACTIVATE_VENV) yardstick -v YARDSTICK_RESULT_DIR = .yardstick/result YARDSTICK_LABELS_DIR = .yardstick/labels VULNERABILITY_LABELS = ./vulnerability-labels RESULT_SET = pr_vs_latest_via_sbom # update periodically with values from "grype db list" TEST_DB_URL_FILE = ./test-db TEST_DB_URL = "$(shell cat $(TEST_DB_URL_FILE))" TEST_DB = db.tar.zst # formatting variables BOLD := $(shell tput -T linux bold) PURPLE := $(shell tput -T linux setaf 5) GREEN := $(shell tput -T linux setaf 2) CYAN := $(shell tput -T linux setaf 6) RED := $(shell tput -T linux setaf 1) RESET := $(shell tput -T linux sgr0) TITLE := $(BOLD)$(PURPLE) SUCCESS := $(BOLD)$(GREEN) .PHONY: all all: capture validate ## Fetch or capture all data and run all quality checks .PHONY: validate validate: venv $(VULNERABILITY_LABELS)/Makefile ## Run all quality checks against already collected data $(YARDSTICK) validate -r $(RESULT_SET) -r $(RESULT_SET)_2022 -r $(RESULT_SET)_2024 .PHONY: capture capture: sboms vulns ## Collect and store all syft and grype results .PHONY: vulns vulns: venv $(TEST_DB) ## Collect and store all grype results $(YARDSTICK) -v result capture -r $(RESULT_SET) $(YARDSTICK) -v result capture -r $(RESULT_SET)_2022 $(YARDSTICK) -v result capture -r $(RESULT_SET)_2024 $(TEST_DB): @curl -o $(TEST_DB) -SsL $(TEST_DB_URL) .PHONY: sboms sboms: $(YARDSTICK_RESULT_DIR) venv clear-results ## Collect and store all syft results (deletes all existing results) bash -c "make download-sboms || ($(YARDSTICK) -v result capture -r $(RESULT_SET) --only-producers && $(YARDSTICK) -v result capture -r $(RESULT_SET)_2022 -r $(RESULT_SET)_2024 --only-producers)" .PHONY: download-sboms download-sboms: $(VULNERABILITY_LABELS)/Makefile cd vulnerability-match-labels && make venv bash -c "export ORAS_CACHE=$(shell pwd)/.oras-cache && make venv && . vulnerability-match-labels/venv/bin/activate && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET) && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET)_2022 && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET)_2024" venv: venv/touchfile venv/touchfile: requirements.txt test -d venv || python3 -m venv venv $(ACTIVATE_VENV) pip install -Ur requirements.txt touch venv/touchfile $(YARDSTICK_RESULT_DIR): mkdir -p $(YARDSTICK_RESULT_DIR) $(VULNERABILITY_LABELS)/Makefile: git submodule update vulnerability-match-labels .PHONY: clear-results clear-results: venv ## Clear all existing yardstick results $(YARDSTICK) result clear .PHONY: clean clean: clear-results ## Clear all existing yardstick results and delete python environment rm -rf venv find -iname "*.pyc" -delete help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' ================================================ FILE: test/quality/README.md ================================================ # Match quality testing This form of testing compares the results from various releases of grype using a static set of reference container images. The kinds of comparisons made are: 1) "relative": find the vulnerability matching differences between both tools for a given image. This helps identify when a change has occurred in matching behavior and where the changes are. 2) "against labels": pair each tool results for an image with ground truth. This helps identify how well the matching behavior is performing (did it get better or worse). ## Getting started For information about required setup see: [Required setup](#required-setup). To capture raw tool output and store into the local `.yardstick` directory for further analysis: ``` make capture ``` To analyze the tool output and evaluate a pass/fail result: ``` make validate ``` A pass/fail result is shown in the output with reasons for the failure being listed explicitly. ## What is the quality gate criteria The label comparison results are used to determine a pass/fail result, specifically with the following criteria: - fail when current grype F1 score drops below last grype release F1 score (or F1 score is indeterminate) - fail when the indeterminate matches % > 10% in the current grype results - fail when there is a rise in FNs relative to the results from the last grype release - otherwise, pass F1 score is the primary way that tool matching performance is characterized. F1 score combines the TP, FP, and FN counts into a single metric between 0 and 1. Ideally the F1 score for an image-tool pair should be 1. F1 score is a good way to summarize the matching performance but does not explain why the matching performance is what it is. Indeterminate matches are matches from results that could not be paired with a label (TP or FP). This could also mean that multiple conflicting labels were found for a single match. The more indeterminate matches there are the less confident you can be about the F1 score. Ideally there should be 0 indeterminate matches, but this is difficult to achieve since vulnerability data is constantly changing. False negatives represent matches that should have been made by the tool but were missed. We should always make certain that this value does not increase between releases of grype. ## Assumptions 1. **Comparing vulnerability results taken at different times is invalid**. We leverage the yardstick result-set feature to capture all vulnerability results at one time for a specific image and tool set. Why? If we use grype at version `a` on monday and grype at version `b` on tuesday and attempt to compare the results, if differences are found it will not be immediately clear why the results are different. That is, it is entirely possible that the vulnerability databases from the run of `b` simply had more up to date information, and if `grype@a` were run at the same time (on tuesday) this reason can be almost entirely eliminated. 2. **Comparing vulnerability results across images with different digests is invalid**. It may be very tempting to compare vulnerability results for `alpine:3.2` from monday and `alpine:3.2` from tuesday to see if there are any changes. However, this is potentially inaccurate as the image references are for the same tag, but the publisher may have pushed a new image with differing content. Any change could lead to different vulnerability matching results but we are only interested in vulnerability match differences that are due to actionable reasons (grype matcher logic problems or [SBOM] input data into matchers). ## Approach Vulnerability matching has essentially two inputs: - the packages that were found in the scanned artifact - the vulnerability data from upstream providers (e.g. NVD, GHSA, etc.) These are both moving targets! We may implement more catalogers in syft that raise up more packages discovered over time (for the same artifact scanned). Also the world is continually finding and reporting new vulnerabilities. The more moving parts there are in this form of testing the harder it is to come to a conclusion about the actual quality of the output over time. To reduce the eroding value over time we've decided to change as many moving targets into fixed targets as possible: - Vulnerability results beyond a particular year are ignored (the current config allows for <= 2020). Though there are still retroactive CVEs created, this helps a lot in terms of keeping vulnerability results relatively stable. - SBOMs are used as input into grype instead of the raw container images. This allows the artifacts under test to remain truly fixed and saves a lot of time when capturing grype results (as the container image is no longer needed during analysis). - For the captured SBOMs, container images referenced must be with a digest, not just a tag. In case we update a tool version (say syft) we want to make certain that we are scanning the exact same artifact later when we re-run the analysis. - Versions of tools used are fixed to a specific `major.minor.patch` release used. This allows us to account for capability differences between tool runs. To reduce maintenance effort of this comparison over time there are a few things to keep in mind: - Once an image is labeled (at a specific digest) the image digest should be considered immutable (never updated). Why? It takes a lot of effort to label images and there are no "clearly safe" assumptions that can be made when it comes to migrating labels from one image to another no matter how "similar" the images may be. There is also no value in updating the image; these images are not being executed and their only purpose is to survey the matching performance of grype. In the philosophy of "maximizing fixed points" it doesn't make sense to change these assets. Over time it may be that we remove assets that are no longer useful for comparison, but this should rarely be done. - Consider not changing the CVE year max-ceiling (currently set to 2020). Pushing this ceiling will likely raise the number of unlabled matches significantly for all images. Only bump this ceiling if all possible matches are labeled. ## Workflow One way of working is to simply run `yardstick` and `gate.py` in the `test/quality` directory. You will need to make sure the `vulnerability-match-labels` submodule has been initialized. This happens automatically for some `make` commands, but you can ensure this by `git submodule update --init`. After the submodule has been initialized, the match data from `vulnerability-match-labels` will be available locally. **TIP**: when dealing with submodules, it may be convenient to set the git config option `submodule.recurse` to `true` so `git checkout` will automatically update submodules to the correct commit: ```shell git config submodule.recurse true ``` To do this we need some results to begin with. As noted above, start with (this does ensure the submodule is initialized): ```shell make capture ``` This will download prebuilt SBOMs for the configured images and generate match results for configured tools (here: the previous Grype version as well as the local version). After `make capture` has finished, we should have results and can now start inspecting and modifying the comparison labels. To get started, let's assume we see some quality gate failure in like this (something found in CI or after running `./gate.py`): ``` Running comparison against labels... Results used: ├── f4fb4e6e-c911-41b6-9a10-f90b3954a41a : grype@v0.53.1-19-g8900767 against docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 └── fcebdd0b-d80a-4fe2-b81a-802c7b98d83b : grype@v0.53.1 against docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 Match differences between tooling (with labels): TOOL PARTITION PACKAGE VULNERABILITY LABEL COMMENTARY grype@v0.53.1 ONLY node@14.18.2 CVE-2021-44531 TruePositive (this is a new FN 😱) grype@v0.53.1 ONLY node@14.18.2 CVE-2021-44532 TruePositive (this is a new FN 😱) grype@v0.53.1 ONLY node@14.18.2 CVE-2021-44533 TruePositive (this is a new FN 😱) Failed quality gate - current F1 score is lower than the latest release F1 score: current=0.80 latest=0.80 image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 - current indeterminate matches % is greater than 10%: current=13.60% image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 - current false negatives is greater than the latest release false negatives: current=6 latest=3 image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 ``` This tells us some important information: which package, version, and vulnerability had a difference; how it was previously labeled, and most importantly: the image we need to focus on (`docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9`). Using the SHA above, we can run `yardstick` to see which results are available: ```shell $ yardstick result list --result-set pr_vs_latest_via_sbom | grep 808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 5bf0611b-183f-4525-a1ab-f268f62f48b6 docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 grype@v0.53.1 2022-12-09 20:49:56+00:00 43a9650a-d5de-4687-b3ba-459105e32cb8 docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 grype@v0.53.1-15-gf29a32b 2022-12-09 20:49:53+00:00 67913f57-690f-4f35-a2d9-ffccd2a0b2a1 docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 syft@v0.60.1 2022-11-01 20:30:52+00:00 ``` We'll need to use the UUIDs to explore the labels, so copy the first UUID, which we can see was run against the last Grype release (`grype@v0.53.1`). Use the UUID to explore and edit the results with `yardstick label explore`: ```shell yardstick label explore 5bf0611b-183f-4525-a1ab-f268f62f48b6 ``` At this point we can use the TUI to explore and modify the match data, by deleting things or labeling as true positives, false positives, etc.. **After making changes make sure to save the results** (`Ctrl-S`)! At this point you can run the quality gate using updated label data. The quality gate can run against just one image, for example the image we first found in the failure, so run the quality gate and see how changes to the label data have affected the result: ```shell ./gate.py --image docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9 ``` After iterating on all the changes we need using `yardstick label explore`, we're now ready to commit the changes. Since we're using `git submodules`, we need to complete two steps: 1. get the changes merged to the `vulnerability-match-labels` repository `main` branch 2. update the submodule in this repository To create a pull request for the `vulnerability-match-labels`, make sure you are in the `vulnerability-match-labels` subdirectory and create a branch -- something like: ```shell git checkout --no-track -b my-branch-name ``` Commit the changes to this branch, push, create a pull request like normal. NOTE: you may need to add a fork (`git remote add ...`) and push to the fork if you don't have push permissions against the main `vulnerability-match-labels` repo. After the PR is approved and merged to `vulnerability-match-labels` repo's `main` branch, update the submodule locally using: ```shell git submodule update --remote ``` Next, _commit the submodule change_ as part of any other changes to the Grype pull request and push as part of the in-progress PR against Grype. The PR will now use the updated match labels when running the quality check. ## Required setup In order to manage Python versions, [pyenv](https://github.com/pyenv/pyenv) can be used. (e.g. `brew install pyenv`) Both this project and `yardstick` require Python 3.10. Using `pyenv`, see which python versions are available, for example: ```shell $ pyenv install --list|grep 3.10 3.10.0 ... 3.10.7 ... ``` In this case, we see `3.10.7` is the latest version, so we'll use that for the rest of the setup: Install this version using `pyenv`: ```shell pyenv install 3.10.7 ``` NOTE: to view the specific Python versions installed use `pyenv versions`: ```shell $ pyenv versions system * 3.8.13 (set by /Users/usr/.pyenv/version) 3.10.7 ``` To select the `3.10` version use the exact version number: ```shell pyenv shell 3.10.7 ``` (or maybe just: `pyenv shell $(pyenv versions | grep 3.10 | tail -1)`) Verify this has worked properly by running: ```shell python --version ``` **Important:** it is also required to have `oras` installed (e.g. `brew install oras`) **After** setting the working Python version to 3.10, in the `test/quality` directory, you need to set up a virtual environment using: ```shell make venv ``` **After** creating the virtual environment, you can now activate it to set up a working shell using: ```shell . venv/bin/activate ``` You should now have a shell running in the correct virtual environment, it might look something like this: ```shell (venv) user@HOST quality % ``` Now you should be able to run both `yardstick` and `./gate.py`. ## Troubleshooting As noted above, yardstick requires Python 3.10. If you try to run with an older version, such as the default macOS 3.8 version, you will likely see an error similar to: ``` Traceback (most recent call last): File "./vulnerability-match-labels/sboms.py", line 12, in import yardstick File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/__init__.py", line 4, in from . import arrange, artifact, capture, cli, comparison, label, store, tool, utils File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/arrange.py", line 4, in from yardstick import artifact File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/artifact.py", line 482, in class ResultSet: File "/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/artifact.py", line 484, in ResultSet state: list[ResultState] = field(default_factory=list) TypeError: 'type' object is not subscriptable ``` ================================================ FILE: test/quality/requirements.txt ================================================ yardstick==v0.15.0 # ../../../yardstick tabulate==0.9.0 dataclass-wizard==0.36.2 ================================================ FILE: test/quality/test-db ================================================ https://grype.anchore.io/databases/v6/vulnerability-db_v6.1.4_2026-03-01T00:32:26Z_1772346262.tar.zst ================================================ FILE: test/validate-grype-db-schema.py ================================================ #!/usr/bin/env python import re import os import sys import collections dir_pattern = r'grype/db/v(?P\d+)' db_dir_regex = re.compile(dir_pattern) import_regex = re.compile(rf'github.com/anchore/grype/{dir_pattern}') def report_schema_versions_found(title, schema_to_locations): for schema, locations in sorted(schema_to_locations.items()): print(f"{title} schema: {schema}") for location in locations: print(f" {location}") print() def assert_single_schema_version(schema_to_locations): schema_versions_found = list(schema_to_locations.keys()) try: for x in schema_versions_found: int(x) except ValueError: sys.exit("Non-numeric schema found: %s" % ", ".join(schema_versions_found)) if len(schema_to_locations) > 1: sys.exit("Found multiple schemas: %s" % ", ".join(schema_versions_found)) elif len(schema_to_locations) == 0: sys.exit("No schemas found!") def find_db_schema_usages(filter_out_regexes=None, keep_regexes=None): schema_to_locations = collections.defaultdict(list) for root, dirs, files in os.walk("."): for file in files: if not file.endswith(".go"): continue location = os.path.join(root, file) if filter_out_regexes: do_filter = False for regex in filter_out_regexes: if regex.findall(location): do_filter = True break if do_filter: continue if keep_regexes: do_keep = False for regex in keep_regexes: if regex.findall(location): do_keep = True break if not do_keep: continue # keep track of all of the imports (from this point on, this is only possible consumers of db/v# code with open(location) as f: for match in import_regex.findall(f.read(), re.MULTILINE): schema_to_locations[match].append(location) return schema_to_locations def assert_schema_version_prefix(schema, locations): for location in locations: if f"/grype/db/v{schema}" not in location: sys.exit(f"found cross-schema reference: {location}") def validate_schema_consumers(): schema_to_locations = find_db_schema_usages(filter_out_regexes=[db_dir_regex]) report_schema_versions_found("Consumers of", schema_to_locations) assert_single_schema_version(schema_to_locations) print("Consuming schema versions found: %s" % list(schema_to_locations.keys())[0]) def validate_schema_definitions(): schema_to_locations = find_db_schema_usages(keep_regexes=[db_dir_regex]) report_schema_versions_found("Definitions of", schema_to_locations) # make certain that each definition keeps out of other schema definitions for schema, locations in schema_to_locations.items(): assert_schema_version_prefix(schema, locations) print("Verified that schema definitions don't cross-import") def main(): validate_schema_definitions() print() validate_schema_consumers() if __name__ == "__main__": main()