-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ed2337f
Showing
6 changed files
with
515 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__version__ = "0.0.1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") |
Oops, something went wrong.