Skip to content

Commit 8d25c80

Browse files
authored
Merge pull request #332 from pyupio/nicholas/adding-bare-json-to-license-command
Adding bare and json outputs to license command
2 parents 23ac3a3 + b27a273 commit 8d25c80

File tree

6 files changed

+147
-12
lines changed

6 files changed

+147
-12
lines changed

HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ History
66
-------------------
77

88
* Current unstable version
9+
* Added bare and json outputs to license command
910

1011
1.10.0 (2020-12-20)
1112
-------------------

safety/cli.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,17 @@ def review(full_report, bare, file):
123123

124124

125125
@cli.command()
126-
@click.option("--key", required=True, envvar="SAFETY_API_KEY",
126+
@click.option("--key", envvar="SAFETY_API_KEY",
127127
help="API Key for pyup.io's vulnerability database. Can be set as SAFETY_API_KEY "
128128
"environment variable. Default: empty")
129129
@click.option("--db", default="",
130130
help="Path to a local license database. Default: empty")
131+
@click.option("--json/--no-json", default=False,
132+
help="Output packages licenses in JSON format. Default: --no-json")
133+
@click.option("--bare/--not-bare", default=False,
134+
help='Output packages licenses names only. '
135+
'Useful in combination with other tools. '
136+
'Default: --not-bare')
131137
@click.option("--cache/--no-cache", default=True,
132138
help='Whether license database file should be cached.'
133139
'Default: --cache')
@@ -139,7 +145,7 @@ def review(full_report, bare, file):
139145
help="Proxy port number --proxy-port")
140146
@click.option("proxyprotocol", "--proxy-protocol", "-pr", multiple=False, type=str, default='http',
141147
help="Proxy protocol (https or http) --proxy-protocol")
142-
def license(key, db, cache, files, proxyprotocol, proxyhost, proxyport):
148+
def license(key, db, json, bare, cache, files, proxyprotocol, proxyhost, proxyport):
143149

144150
if files:
145151
packages = list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
@@ -153,11 +159,14 @@ def license(key, db, cache, files, proxyprotocol, proxyhost, proxyport):
153159
proxy_dictionary = get_proxy_dict(proxyprotocol, proxyhost, proxyport)
154160
try:
155161
licenses_db = safety.get_licenses(key, db, cache, proxy_dictionary)
156-
except InvalidKeyError:
157-
click.secho("Your API Key '{key}' is invalid. See {link}".format(
158-
key=key, link='https://goo.gl/O7Y1rS'),
159-
fg="red",
160-
file=sys.stderr)
162+
except InvalidKeyError as invalid_key_error:
163+
if str(invalid_key_error):
164+
message = str(invalid_key_error)
165+
else:
166+
message = "Your API Key '{key}' is invalid. See {link}".format(
167+
key=key, link='https://goo.gl/O7Y1rS'
168+
)
169+
click.secho(message, fg="red", file=sys.stderr)
161170
sys.exit(-1)
162171
except DatabaseFileNotFoundError:
163172
click.secho("Unable to load licenses database from {db}".format(db=db), fg="red", file=sys.stderr)
@@ -172,7 +181,12 @@ def license(key, db, cache, files, proxyprotocol, proxyhost, proxyport):
172181
click.secho("Unable to load licenses database", fg="red", file=sys.stderr)
173182
sys.exit(-1)
174183
filtered_packages_licenses = get_packages_licenses(packages, licenses_db)
175-
output_report = license_report(packages=packages, licenses=filtered_packages_licenses)
184+
output_report = license_report(
185+
packages=packages,
186+
licenses=filtered_packages_licenses,
187+
json_report=json,
188+
bare_report=bare
189+
)
176190
click.secho(output_report, nl=True)
177191

178192

safety/formatter.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ class JsonReport(object):
236236
@staticmethod
237237
def render(vulns, full):
238238
return json.dumps(vulns, indent=4, sort_keys=True)
239+
240+
@staticmethod
241+
def render_licenses(packages_licenses):
242+
return json.dumps(packages_licenses, indent=4, sort_keys=True)
239243

240244

241245
class BareReport(object):
@@ -244,6 +248,14 @@ class BareReport(object):
244248
def render(vulns, full):
245249
return " ".join(set([v.name for v in vulns]))
246250

251+
@staticmethod
252+
def render_licenses(packages_licenses):
253+
licenses = set([pkg_li.get('license') for pkg_li in packages_licenses])
254+
if "N/A" in licenses:
255+
licenses.remove("N/A")
256+
sorted_licenses = sorted(licenses)
257+
return " ".join(sorted_licenses)
258+
247259

248260
def get_used_db(key, db):
249261
key = key if key else os.environ.get("SAFETY_API_KEY", False)
@@ -266,9 +278,13 @@ def report(vulns, full=False, json_report=False, bare_report=False, checked_pack
266278
return BasicReport.render(vulns, full=full, checked_packages=checked_packages, used_db=used_db)
267279

268280

269-
def license_report(packages, licenses):
270-
size = get_terminal_size()
281+
def license_report(packages, licenses, json_report=False, bare_report=False):
282+
if json_report:
283+
return JsonReport.render_licenses(packages_licenses=licenses)
284+
elif bare_report:
285+
return BareReport.render_licenses(packages_licenses=licenses)
271286

287+
size = get_terminal_size()
272288
if size.columns >= 80:
273289
return SheetReport.render_licenses(packages, licenses)
274290
return BasicReport.render_licenses(packages, licenses)

safety/safety.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def get_licenses(key, db_mirror, cached, proxy):
190190
key = key if key else os.environ.get("SAFETY_API_KEY", False)
191191

192192
if not key and not db_mirror:
193-
raise InvalidKeyError("API-KEY not provided.")
193+
raise InvalidKeyError("The API-KEY was not provided.")
194194
if db_mirror:
195195
mirrors = [db_mirror]
196196
else:

tests/reqs_4.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
django==1.11

tests/test_safety.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,42 @@ def test_review_fail(self):
5454
result = runner.invoke(cli.cli, ['review', '--bare', '--file', path_to_report])
5555
assert result.exit_code == -1
5656

57+
@patch("safety.safety.get_licenses")
58+
def test_license_bare(self, get_licenses):
59+
runner = CliRunner()
60+
61+
dirname = os.path.dirname(__file__)
62+
with open(os.path.join(dirname, "test_db", "licenses.json")) as f:
63+
licenses_db = json.loads(f.read())
64+
get_licenses.return_value = licenses_db
65+
reqs_path = os.path.join(dirname, "reqs_4.txt")
66+
67+
result = runner.invoke(cli.cli, ['license', '--file', reqs_path, '--bare', '--db', 'licenses.json'])
68+
self.assertEqual(result.exit_code, 0)
69+
self.assertEqual(result.output, 'BSD-3-Clause\n')
70+
71+
@patch("safety.safety.get_licenses")
72+
def test_license_json(self, get_licenses):
73+
runner = CliRunner()
74+
75+
dirname = os.path.dirname(__file__)
76+
with open(os.path.join(dirname, "test_db", "licenses.json")) as f:
77+
licenses_db = json.loads(f.read())
78+
get_licenses.return_value = licenses_db
79+
reqs_path = os.path.join(dirname, "reqs_4.txt")
80+
81+
result = runner.invoke(cli.cli, ['license', '--file', reqs_path, '--json', '--db', 'licenses.json'])
82+
expected_result = json.dumps(
83+
[{
84+
"license": "BSD-3-Clause",
85+
"package": "django",
86+
"version": "1.11"
87+
}],
88+
indent=4, sort_keys=True
89+
)
90+
self.assertEqual(result.exit_code, 0)
91+
self.assertMultiLineEqual(result.output.rstrip(), expected_result)
92+
5793

5894
class TestFormatter(unittest.TestCase):
5995

@@ -269,7 +305,7 @@ def test_get_packages_licenses_without_api_key(self):
269305
key=None
270306
)
271307
db_generic_exception = error.exception
272-
self.assertEqual(str(db_generic_exception), 'API-KEY not provided.')
308+
self.assertEqual(str(db_generic_exception), 'The API-KEY was not provided.')
273309

274310
@patch("safety.safety.requests")
275311
def test_get_packages_licenses_with_invalid_api_key(self, requests):
@@ -387,6 +423,73 @@ def test_get_cached_packages_licenses(self, requests):
387423
self.assertNotEqual(resp, licenses_db)
388424
self.assertEqual(resp, original_db)
389425

426+
def test_report_licenses_bare(self):
427+
from safety.formatter import license_report
428+
429+
reqs = StringIO("Django==1.8.1\n\rinexistent==1.0.0")
430+
packages = util.read_requirements(reqs)
431+
432+
# Using DB: test.test_db.licenses.json
433+
licenses_db = safety.get_licenses(
434+
db_mirror=os.path.join(
435+
os.path.dirname(os.path.realpath(__file__)),
436+
"test_db"
437+
),
438+
cached=False,
439+
key=None,
440+
proxy={},
441+
)
442+
443+
pkgs_licenses = util.get_packages_licenses(packages, licenses_db)
444+
output_report = license_report(
445+
packages=packages,
446+
licenses=pkgs_licenses,
447+
json_report=False,
448+
bare_report=True
449+
)
450+
self.assertEqual(output_report, "BSD-3-Clause")
451+
452+
def test_report_licenses_json(self):
453+
from safety.formatter import license_report
454+
455+
reqs = StringIO("Django==1.8.1\n\rinexistent==1.0.0")
456+
packages = util.read_requirements(reqs)
457+
458+
# Using DB: test.test_db.licenses.json
459+
licenses_db = safety.get_licenses(
460+
db_mirror=os.path.join(
461+
os.path.dirname(os.path.realpath(__file__)),
462+
"test_db"
463+
),
464+
cached=False,
465+
key=None,
466+
proxy={},
467+
)
468+
469+
pkgs_licenses = util.get_packages_licenses(packages, licenses_db)
470+
output_report = license_report(
471+
packages=packages,
472+
licenses=pkgs_licenses,
473+
json_report=True,
474+
bare_report=False
475+
)
476+
477+
expected_result = json.dumps(
478+
[{
479+
"license": "BSD-3-Clause",
480+
"package": "django",
481+
"version": "1.8.1"
482+
},
483+
{
484+
"license": "N/A",
485+
"package": "inexistent",
486+
"version": "1.0.0"
487+
}],
488+
indent=4, sort_keys=True
489+
)
490+
# Packages without license are reported as "N/A"
491+
self.assertEqual(output_report.rstrip(), expected_result)
492+
390493

391494
class ReadRequirementsTestCase(unittest.TestCase):
392495

0 commit comments

Comments
 (0)