Skip to content

Commit

Permalink
Add subcommand for CVE record validation
Browse files Browse the repository at this point in the history
Can be used to validate a full record, or just a CNA published/rejected
container or an ADP container.
  • Loading branch information
mprpic committed Oct 25, 2024
1 parent 1bfaae9 commit a935bdb
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 1 deletion.
67 changes: 66 additions & 1 deletion cvelib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import requests

from . import __version__
from .cve_api import CveApi, CveRecordValidationError
from .cve_api import CveApi, CveRecord, CveRecordValidationError

# Check CVE IDs to use valid years (first one assigned was in 1999; hopefully we'll transcend
# software vulnerabilities in year 3000+), and disallow ID of 0000 and leading zeros for 4+ IDs
Expand Down Expand Up @@ -1139,6 +1139,71 @@ def users(ctx: click.Context, print_raw: bool, no_header: bool) -> None:
print_table(lines, highlight_header=not no_header)


@cli.command(
help=f"Validate a CVE record against the "
f"{CveRecord.Schemas.V5_SCHEMA.rsplit('_', maxsplit=1)[1].removesuffix('.json')} "
f"CVE JSON (sub)schema."
)
@click.option(
"-j",
"--cve-json",
"cve_json_str",
type=click.STRING,
help="JSON body of CVE record.",
)
@click.option(
"-f",
"--cve-json-file",
type=click.File(),
help="File containing JSON body of a CVE record.",
)
@click.option(
"-s",
"--schema-type",
default="cna-published",
show_default=True,
type=click.Choice(["full", "cna-published", "cna-rejected", "adp"], case_sensitive=False),
help="Specific type of schema to validate against",
)
def validate(
cve_json_str: Optional[str], cve_json_file: Optional[TextIO], schema_type: Optional[str]
) -> None:
if cve_json_file is not None and cve_json_str is not None:
raise click.BadParameter(
"cannot use both `-f/--cve-json-file` and `-j/--cve-json` to provide a CVE JSON."
)

try:
if cve_json_str is not None:
cve_json = json.loads(cve_json_str)
elif cve_json_file is not None:
cve_json = json.load(cve_json_file)
else:
raise click.BadParameter(
"must provide CVE JSON data using one of: `-f/--cve-json-file` or `-j/--cve-json`."
)
except json.JSONDecodeError as exc:
print_error(msg="CVE data is not valid JSON", details=str(exc))
return

if schema_type == "full":
schema_path = CveRecord.Schemas.V5_SCHEMA
elif schema_type == "cna-published":
schema_path = CveRecord.Schemas.CNA_PUBLISHED
elif schema_type == "cna-rejected":
schema_path = CveRecord.Schemas.CNA_REJECTED
else:
schema_path = CveRecord.Schemas.ADP

try:
CveRecord.validate(cve_json, schema_path=schema_path)
except CveRecordValidationError as exc:
click.echo(exc)
sys.exit(1)
else:
click.echo("CVE record is valid!")


@cli.command()
@click.pass_context
@handle_cve_api_error
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
import re
from pathlib import Path
from unittest import mock

from click.testing import CliRunner
Expand Down Expand Up @@ -716,6 +718,23 @@ def test_show_org():
)


def test_validate_good():
runner = CliRunner()
example_cve_file = str(Path(__file__).parent / "data/CVEv5_basic-example.json")
result = runner.invoke(
cli, ["validate", "--cve-json-file", example_cve_file, "--schema-type", "full"]
)
assert result.exit_code == 0, result.output
assert result.output == "CVE record is valid!\n"


def test_validate_bad():
runner = CliRunner()
result = runner.invoke(cli, ["validate", "--cve-json", '{"bad": "record"}'])
assert result.exit_code == 1
assert re.search("^Schema validation .* failed:\n", result.output)


def test_exit_on_help():
with mock.patch("cvelib.cli.CveApi.show_org") as show_org:
show_org.return_value = {}
Expand Down

0 comments on commit a935bdb

Please sign in to comment.