diff --git a/cve_bin_tool/schemas/json2.json b/cve_bin_tool/schemas/json2.json new file mode 100644 index 0000000000..e926c2e037 --- /dev/null +++ b/cve_bin_tool/schemas/json2.json @@ -0,0 +1,481 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Generated schema for Root", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "metadata": { + "type": "object", + "properties": { + "tool": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ] + }, + "generation_date": { + "type": "string" + }, + "parameter": { + "type": "object", + "properties": { + "options": { + "type": "object", + "properties": { + "help": {}, + "exclude": { + "type": "array", + "items": {} + }, + "version": {}, + "disable-version-check": { + "type": "boolean" + }, + "disable-validation-check": { + "type": "boolean" + }, + "offline": { + "type": "boolean" + }, + "detailed": { + "type": "boolean" + } + }, + "required": [ + "help", + "exclude", + "version", + "disable-version-check", + "disable-validation-check", + "offline", + "detailed" + ] + }, + "cve_data_download": { + "type": "object", + "properties": { + "nvd": { + "type": "string" + }, + "update": { + "type": "string" + }, + "nvd-api-key": { + "type": "string" + }, + "disable-data-source": { + "type": "array", + "items": {} + }, + "use-mirror": { + "type": "string" + } + }, + "required": [ + "nvd", + "update", + "nvd-api-key", + "disable-data-source", + "use-mirror" + ] + }, + "input": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "input-file": { + "type": "string" + }, + "triage-input-file": { + "type": "string" + }, + "config": { + "type": "string" + }, + "package-list": { + "type": "string" + }, + "sbom": { + "type": "string" + }, + "sbom-file": { + "type": "string" + } + }, + "required": [ + "directory", + "input-file", + "triage-input-file", + "config", + "package-list", + "sbom", + "sbom-file" + ] + }, + "output": { + "type": "object", + "properties": { + "quiet": { + "type": "boolean" + }, + "log-level": { + "type": "string" + }, + "output-file": { + "type": "string" + }, + "html-theme": { + "type": "string" + }, + "format": { + "type": "string" + }, + "generate-config": { + "type": "string" + }, + "cvss": { + "type": "number" + }, + "severity": { + "type": "string" + }, + "metrics": { + "type": "boolean" + }, + "epss-percentile": {}, + "epss-probability": {}, + "no-0-cve-report": { + "type": "boolean" + }, + "available-fix": { + "type": "string" + }, + "backport-fix": { + "type": "string" + }, + "affected-versions": { + "type": "number" + }, + "vex": { + "type": "string" + }, + "sbom-output": { + "type": "string" + }, + "sbom-type": { + "type": "string" + }, + "sbom-format": { + "type": "string" + } + }, + "required": [ + "quiet", + "log-level", + "output-file", + "html-theme", + "format", + "generate-config", + "cvss", + "severity", + "metrics", + "epss-percentile", + "epss-probability", + "no-0-cve-report", + "available-fix", + "backport-fix", + "affected-versions", + "vex", + "sbom-output", + "sbom-type", + "sbom-format" + ] + }, + "merge_report": { + "type": "object", + "properties": { + "append": { + "type": "boolean" + }, + "tag": { + "type": "string" + }, + "merge": {}, + "filter": { + "type": "array", + "items": {} + } + }, + "required": [ + "append", + "tag", + "merge", + "filter" + ] + }, + "checkers": { + "type": "object", + "properties": { + "skips": { + "type": "string" + }, + "runs": { + "type": "string" + } + }, + "required": [ + "skips", + "runs" + ] + }, + "database_management": { + "type": "object", + "properties": { + "import-json": { + "type": "string" + }, + "ignore-sig": { + "type": "boolean" + }, + "log-signature-error": { + "type": "boolean" + }, + "verify": { + "type": "string" + }, + "export-json": { + "type": "string" + }, + "pgp-sign": { + "type": "string" + }, + "passphrase": { + "type": "string" + }, + "export": { + "type": "string" + }, + "import": { + "type": "string" + } + }, + "required": [ + "import-json", + "ignore-sig", + "log-signature-error", + "verify", + "export-json", + "pgp-sign", + "passphrase", + "export", + "import" + ] + }, + "exploits": { + "type": "object", + "properties": { + "exploits": { + "type": "boolean" + } + }, + "required": [ + "exploits" + ] + }, + "deprecated": { + "type": "object", + "properties": { + "extract": { + "type": "boolean" + }, + "report": { + "type": "boolean" + } + }, + "required": [ + "extract", + "report" + ] + } + }, + "required": [ + "options", + "cve_data_download", + "input", + "output", + "merge_report", + "checkers", + "database_management", + "exploits", + "deprecated" + ] + } + }, + "required": [ + "tool", + "generation_date", + "parameter" + ] + }, + "database_info": { + "type": "object", + "properties": { + "last_updated": { + "type": "string" + }, + "total_entries": { + "type": "object", + "properties": { + "NVD": { + "type": "number" + }, + "OSV": { + "type": "number" + }, + "GAD": { + "type": "number" + }, + "REDHAT": { + "type": "number" + } + }, + "required": [ + "NVD" + ] + } + }, + "required": [ + "last_updated", + "total_entries" + ] + }, + "vulnerabilities": { + "type": "object", + "properties": { + "summary": { + "type": "object", + "properties": { + "CRITICAL": { + "type": "number" + }, + "HIGH": { + "type": "number" + }, + "MEDIUM": { + "type": "number" + }, + "LOW": { + "type": "number" + }, + "UNKNOWN": { + "type": "number" + } + }, + "required": [ + "CRITICAL", + "HIGH", + "MEDIUM", + "LOW", + "UNKNOWN" + ] + }, + "report": { + "type": "array", + "items": { + "type": "object", + "properties": { + "datasource": { + "type": "string" + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "vendor": { + "type": "string" + }, + "product": { + "type": "string" + }, + "version": { + "type": "string" + }, + "cve_number": { + "type": "string" + }, + "severity": { + "type": "string" + }, + "score": { + "type": "string" + }, + "source": { + "type": "string" + }, + "cvss_version": { + "type": "string" + }, + "cvss_vector": { + "type": "string" + }, + "paths": { + "type": "string" + }, + "remarks": { + "type": "string" + }, + "comments": { + "type": "string" + } + }, + "required": [ + "vendor", + "product", + "version", + "cve_number", + "severity", + "score", + "source", + "cvss_version", + "cvss_vector", + "paths", + "remarks", + "comments" + ] + } + } + }, + "required": [ + "datasource", + "entries" + ] + } + } + }, + "required": [ + "summary", + "report" + ] + } + }, + "required": [ + "$schema", + "metadata", + "database_info", + "vulnerabilities" + ] +} \ No newline at end of file diff --git a/test/test_output_engine.py b/test/test_output_engine.py index 5a65e5c4cd..8bd1864fb1 100644 --- a/test/test_output_engine.py +++ b/test/test_output_engine.py @@ -14,11 +14,13 @@ from pathlib import Path from unittest.mock import MagicMock, call, patch +from jsonschema import validate +from jsonschema.exceptions import ValidationError from rich.console import Console from cve_bin_tool.output_engine import OutputEngine, output_csv, output_pdf from cve_bin_tool.output_engine.console import output_console -from cve_bin_tool.output_engine.json_output import output_json +from cve_bin_tool.output_engine.json_output import output_json, output_json2 from cve_bin_tool.output_engine.util import format_output from cve_bin_tool.sbom_manager.generate import SBOMGenerate from cve_bin_tool.util import CVE, CVEData, ProductInfo, Remarks, VersionInfo @@ -465,6 +467,75 @@ class TestOutputEngine(unittest.TestCase): "CVE-9999-0008": VersionInfo("", "", "", "1.2.0"), } + MOCK_ORGANIZED_PARAMETERS = { + "options": { + "help": {"arg_value": None}, + "exclude": {"arg_value": []}, + "version": {"arg_value": None}, + "disable-version-check": {"arg_value": False}, + "disable-validation-check": {"arg_value": False}, + "offline": {"arg_value": False}, + "detailed": {"arg_value": False}, + }, + "cve_data_download": { + "nvd": {"arg_value": "json-mirror"}, + "update": {"arg_value": "daily"}, + "nvd-api-key": {"arg_value": ""}, + "disable-data-source": {"arg_value": []}, + "use-mirror": {"arg_value": ""}, + }, + "input": { + "directory": {"arg_value": "test/language_data/pubspec.lock"}, + "input-file": {"arg_value": ""}, + "triage-input-file": {"arg_value": ""}, + "config": {"arg_value": ""}, + "package-list": {"arg_value": ""}, + "sbom": {"arg_value": ""}, + "sbom-file": {"arg_value": ""}, + }, + "output": { + "quiet": {"arg_value": False}, + "log-level": {"arg_value": "debug"}, + "output-file": {"arg_value": "temp_test_json2"}, + "html-theme": {"arg_value": ""}, + "format": {"arg_value": "json2"}, + "generate-config": {"arg_value": ""}, + "cvss": {"arg_value": 0}, + "severity": {"arg_value": "low"}, + "metrics": {"arg_value": False}, + "epss-percentile": {"arg_value": None}, + "epss-probability": {"arg_value": None}, + "no-0-cve-report": {"arg_value": False}, + "available-fix": {"arg_value": ""}, + "backport-fix": {"arg_value": ""}, + "affected-versions": {"arg_value": 0}, + "vex": {"arg_value": ""}, + "sbom-output": {"arg_value": ""}, + "sbom-type": {"arg_value": "spdx"}, + "sbom-format": {"arg_value": "tag"}, + }, + "merge_report": { + "append": {"arg_value": False}, + "tag": {"arg_value": ""}, + "merge": {"arg_value": None}, + "filter": {"arg_value": []}, + }, + "checkers": {"skips": {"arg_value": ""}, "runs": {"arg_value": ""}}, + "database_management": { + "import-json": {"arg_value": ""}, + "ignore-sig": {"arg_value": False}, + "log-signature-error": {"arg_value": False}, + "verify": {"arg_value": ""}, + "export-json": {"arg_value": ""}, + "pgp-sign": {"arg_value": ""}, + "passphrase": {"arg_value": ""}, + "export": {"arg_value": ""}, + "import": {"arg_value": ""}, + }, + "exploits": {"exploits": {"arg_value": False}}, + "deprecated": {"extract": {"arg_value": True}, "report": {"arg_value": False}}, + } + FORMATTED_OUTPUT = [ { "vendor": "vendor0", @@ -998,6 +1069,36 @@ def test_output_json(self): self.mock_file.seek(0) # reset file position self.assertEqual(json.load(self.mock_file), self.FORMATTED_OUTPUT) + def test_output_json2(self): + """Test formatting output as JSON2""" + output_json2( + self.MOCK_OUTPUT, + None, + datetime.today(), + self.mock_file, + 0, + self.MOCK_ORGANIZED_PARAMETERS, + metrics=True, + ) + + # Load the JSON2 schema + schema_path = ( + Path(__file__).resolve().parent.parent + / "cve_bin_tool" + / "schemas" + / "json2.json" + ) + with open(schema_path, encoding="utf-8") as schema_file: + json2_schema = json.load(schema_file) + + self.mock_file.seek(0) # reset file position + + # Validate -- will raise a ValidationError if not valid and fail the test + try: + validate(json.load(self.mock_file), json2_schema) + except ValidationError as ve: + self.fail(f"Validation error occurred: {ve}") + def test_output_csv(self): """Test formatting output as CSV""" output_csv(self.MOCK_OUTPUT, None, self.mock_file, metrics=True)