Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mprpic committed Dec 18, 2020
0 parents commit ed2337f
Show file tree
Hide file tree
Showing 6 changed files with 515 additions and 0 deletions.
108 changes: 108 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
pip-wheel-metadata/

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# IPython
profile_default/
ipython_config.py

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Editors
.idea/
1 change: 1 addition & 0 deletions cvelib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
274 changes: 274 additions & 0 deletions cvelib/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import json
import re
import sys
from datetime import date, datetime

import click

from .idr import Idr, IdrException

CVE_RE = re.compile(r"^CVE-[12]\d{3}-\d{4,}$")
CONTEXT_SETTINGS = {
"help_option_names": ["-h", "--help"],
"show_default": True,
"max_content_width": 100,
}


def validate_cve(ctx, param, value):
if value is None:
return
if not CVE_RE.match(value):
raise click.BadParameter("invalid CVE ID")
return value


def validate_year(ctx, param, value):
if value is None:
return
# Hopefully this code won't be around in year 10,000.
if not re.match(r"^[1-9]\d{3}$", value):
raise click.BadParameter("invalid year")
return value


def print_ts(ts):
return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S.%f%z").strftime("%c")


def natural_cve_sort(cve):
if not cve:
return []
return [int(x) for x in cve.split("-")[1:]]


class Config:
def __init__(self, username, org, api_key, env, idr_url, interactive):
self.username = username
self.org = org
self.api_key = api_key
self.env = env
self.idr_url = idr_url
self.interactive = interactive

def init_idr(self, **kwargs):
return Idr(
username=self.username,
org=self.org,
api_key=self.api_key,
env=self.env,
url=self.idr_url,
**kwargs,
)


pass_config = click.make_pass_decorator(Config)


@click.group(context_settings=CONTEXT_SETTINGS)
@click.option("-u", "--username", envvar="CVE_USER", required=True, help="User name")
@click.option("-o", "--org", envvar="CVE_ORG", required=True, help="CNA organization short name")
@click.option("-a", "--api-key", envvar="CVE_API_KEY", required=True, help="API key")
@click.option(
"-e",
"--env",
envvar="CVE_ENVIRONMENT",
default="prod",
type=click.Choice(["prod", "dev"]),
help="Select deployment environment to query.",
)
@click.option(
"--idr-url",
envvar="CVE_IDR_URL",
help="Provide arbitrary URL for the IDR service.",
)
@click.option(
"-i",
"--interactive",
envvar="CVE_INTERACTIVE",
default=False,
is_flag=True,
help="Confirm create/update actions before execution.",
)
@click.pass_context
def cli(ctx, username, org, api_key, env, idr_url, interactive):
"""A CLI interface for the CVE Project services."""
ctx.obj = Config(username, org, api_key, env, idr_url, interactive)


@cli.command(context_settings=CONTEXT_SETTINGS)
@click.option(
"-r",
"--random",
default=False,
is_flag=True,
help="Reserve multiple CVE IDs non-sequentially.",
)
@click.option(
"-y",
"--year",
default=lambda: str(date.today().year),
callback=validate_year,
help="Reserve CVE ID(s) for a given year.",
show_default="current year",
)
@click.option("--raw", "print_raw", default=False, is_flag=True, help="Print response JSON.")
@click.argument("count", default=1, type=click.IntRange(min=1))
@pass_config
def reserve(ctx, random, year, count, print_raw):
"""Reserve one or more CVE IDs.
CVE IDs can be reserved one by one (the lowest IDs are reserved first) or in batches of
multiple IDs per single request. When reserving multiple IDs, you can request those IDs to be
generated sequentially or non-sequentially.
For more information, see: "Developer Guide to CVE Services API" (https://git.io/JLcmZ)
"""
if random and count > 10:
raise click.BadParameter("requesting non-sequential CVE IDs is limited to 10 per request")

if ctx.interactive:
click.echo("You are about to reserve ", nl=False)
if count > 1:
click.secho(
f"{count} {'non-sequential' if random else 'sequential'} ", bold=True, nl=False
)
click.echo("CVE IDs for year ", nl=False)
else:
click.secho("1 ", bold=True, nl=False)
click.echo("CVE ID for year ", nl=False)
click.secho(year, bold=True, nl=False)
click.echo(".")
if not click.confirm("This operation cannot be reversed; do you want to continue?"):
print("Exiting...")
sys.exit(0)

idr = ctx.init_idr()
response = idr.reserve(count, random, year)
cve_data = response.json()

if print_raw:
click.echo(json.dumps(cve_data, indent=4, sort_keys=True))
else:
click.echo("Reserved the following CVE ID(s):\n")
for cve in cve_data["cve_ids"]:
click.echo(cve)

click.echo(f"\nRemaining quota: {response.headers['CVE-API-REMAINING-QUOTA']}")


@cli.command(name="show", context_settings=CONTEXT_SETTINGS)
@click.option("--raw", "print_raw", default=False, is_flag=True, help="Print response JSON.")
@click.argument("cve_id", callback=validate_cve)
@pass_config
def show_cve(ctx, print_raw, cve_id):
"""Display a specific CVE ID owned by your CNA."""
idr = ctx.init_idr()
response = idr.show_cve(cve_id=cve_id)
cve = response.json()

if print_raw:
click.echo(json.dumps(cve, indent=4, sort_keys=True))
else:
click.secho(cve["cve_id"], bold=True)
click.echo(f"├─ State:\t{cve['state']}")
click.echo(f"├─ Owning CNA:\t{cve['owning_cna']}")
click.echo(f"├─ Reserved by:\t{cve['requested_by']['user']} ({cve['requested_by']['cna']})")
click.echo(f"└─ Reserved on:\t{cve['reserved']}")


@cli.command(name="list", context_settings=CONTEXT_SETTINGS)
@click.option("--raw", "print_raw", default=False, is_flag=True, help="Print response JSON.")
@click.option(
"--sort-by",
type=click.Choice(["cve_id", "state", "user", "reserved"], case_sensitive=False),
default="cve_id",
help="Sort output.",
)
@click.option("--year", callback=validate_year, help="Filter by year.")
@click.option(
"--state",
# type=click.Choice(["RESERVED", "PUBLIC", "REJECT"], case_sensitive=False),
help="Filter by reservation state.",
)
@click.option(
"--reserved-lt", type=click.DateTime(), help="Filter by reservation time before timestamp."
)
@click.option(
"--reserved-gt", type=click.DateTime(), help="Filter by reservation time after timestamp."
)
@pass_config
def list_cves(ctx, print_raw, sort_by, **query):
"""Filter and list CVE IDs owned by your CNA."""
idr = ctx.init_idr()
response = idr.list_cves(**query)
cve_data = response.json()

if print_raw:
click.echo(json.dumps(cve_data, indent=4, sort_keys=True))
return

cves = cve_data["cve_ids"]
if sort_by:
key = sort_by.lower()
if key == "user":
cves.sort(key=lambda x: x["requested_by"]["user"])
elif key == "cve_id":
cves.sort(key=lambda x: natural_cve_sort(x["cve_id"]))
elif key == "reserved_asc":
cves.sort(key=lambda x: x["reserved"])
elif key == "state":
cves.sort(key=lambda x: x["state"])

lines = [("CVE ID", "STATE", "OWNING CNA", "REQUESTED BY", "RESERVED")]
for cve in cves:
lines.append(
(
cve["cve_id"],
cve["state"],
cve["owning_cna"],
f"{cve['requested_by']['user']} ({cve['requested_by']['cna']})",
print_ts(cve["reserved"]),
)
)
col_widths = []
for item_index in range(len(lines[0])):
max_len_value = max(lines, key=lambda x: len(x[item_index]))
col_widths.append(len(max_len_value[item_index]))

for idx, line in enumerate(lines):
text = "".join(f"{value:<{width + 3}}" for value, width in zip(line, col_widths)).strip()
if idx == 0:
click.secho(text, bold=True)
else:
click.echo(text)


@cli.command(context_settings=CONTEXT_SETTINGS)
@pass_config
def quota(ctx):
"""Display the available quota for your CNA."""
idr = ctx.init_idr()
response = idr.quota()
idr_quota = response.json()

click.echo("CNA quota for ", nl=False)
click.secho(f"{ctx.org}", bold=True, nl=False)
click.echo(f":\t{idr_quota['id_quota']}")
click.echo(f"├─ Reserved:\t\t{idr_quota['total_reserved']}")
click.echo(f"└─ Available:\t\t{idr_quota['available']}")


@cli.command(context_settings=CONTEXT_SETTINGS)
@pass_config
def ping(ctx):
"""Ping the IDR service to see if it is up."""
idr = ctx.init_idr(raise_exc=False)
idr_status = idr.ping()

click.echo("IDR API Status: ", nl=False)
if idr_status.ok:
click.secho("OK", fg="green")
else:
click.secho(f"NOT OK ({idr_status.status_code})", fg="red")
click.echo(f"└─ {idr.url}")
Loading

0 comments on commit ed2337f

Please sign in to comment.