Skip to content

Commit

Permalink
Merge branch 'master' into RefactorDockerfile
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelpivato committed Dec 27, 2020
2 parents b8b0c8d + 4a8d983 commit ef50ad8
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Dockerfilei386
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ RUN python3 -m pip install pyinstaller pytest

RUN mkdir /app
WORKDIR /app
ADD . ./
COPY . ./

# Install this project dependencies
RUN python3 -m pip install -e .
Expand Down
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ History

* Current unstable version
* Reduced Docker image and Binary size
* Added bare and json outputs to license command

1.10.0 (2020-12-20)
-------------------
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ echo "insecure-package==0.1" | safety check --stdin

*For more examples, take a look at the [options](#options) section.*


### Scan a Python-based Docker image

To scan a docker image `IMAGE_TAG`, you can run

```console
docker run -it --rm ${IMAGE_TAG} "/bin/bash -c \"pip install safety && safety check\"
```

## Using Safety in Docker

Safety can be easily executed as Docker container. It can be used just as
Expand Down
30 changes: 22 additions & 8 deletions safety/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,17 @@ def review(full_report, bare, file):


@cli.command()
@click.option("--key", required=True, envvar="SAFETY_API_KEY",
@click.option("--key", envvar="SAFETY_API_KEY",
help="API Key for pyup.io's vulnerability database. Can be set as SAFETY_API_KEY "
"environment variable. Default: empty")
@click.option("--db", default="",
help="Path to a local license database. Default: empty")
@click.option("--json/--no-json", default=False,
help="Output packages licenses in JSON format. Default: --no-json")
@click.option("--bare/--not-bare", default=False,
help='Output packages licenses names only. '
'Useful in combination with other tools. '
'Default: --not-bare')
@click.option("--cache/--no-cache", default=True,
help='Whether license database file should be cached.'
'Default: --cache')
Expand All @@ -139,7 +145,7 @@ def review(full_report, bare, file):
help="Proxy port number --proxy-port")
@click.option("proxyprotocol", "--proxy-protocol", "-pr", multiple=False, type=str, default='http',
help="Proxy protocol (https or http) --proxy-protocol")
def license(key, db, cache, files, proxyprotocol, proxyhost, proxyport):
def license(key, db, json, bare, cache, files, proxyprotocol, proxyhost, proxyport):

if files:
packages = list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
Expand All @@ -153,11 +159,14 @@ def license(key, db, cache, files, proxyprotocol, proxyhost, proxyport):
proxy_dictionary = get_proxy_dict(proxyprotocol, proxyhost, proxyport)
try:
licenses_db = safety.get_licenses(key, db, cache, proxy_dictionary)
except InvalidKeyError:
click.secho("Your API Key '{key}' is invalid. See {link}".format(
key=key, link='https://goo.gl/O7Y1rS'),
fg="red",
file=sys.stderr)
except InvalidKeyError as invalid_key_error:
if str(invalid_key_error):
message = str(invalid_key_error)
else:
message = "Your API Key '{key}' is invalid. See {link}".format(
key=key, link='https://goo.gl/O7Y1rS'
)
click.secho(message, fg="red", file=sys.stderr)
sys.exit(-1)
except DatabaseFileNotFoundError:
click.secho("Unable to load licenses database from {db}".format(db=db), fg="red", file=sys.stderr)
Expand All @@ -172,7 +181,12 @@ def license(key, db, cache, files, proxyprotocol, proxyhost, proxyport):
click.secho("Unable to load licenses database", fg="red", file=sys.stderr)
sys.exit(-1)
filtered_packages_licenses = get_packages_licenses(packages, licenses_db)
output_report = license_report(packages=packages, licenses=filtered_packages_licenses)
output_report = license_report(
packages=packages,
licenses=filtered_packages_licenses,
json_report=json,
bare_report=bare
)
click.secho(output_report, nl=True)


Expand Down
20 changes: 18 additions & 2 deletions safety/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ class JsonReport(object):
@staticmethod
def render(vulns, full):
return json.dumps(vulns, indent=4, sort_keys=True)

@staticmethod
def render_licenses(packages_licenses):
return json.dumps(packages_licenses, indent=4, sort_keys=True)


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

@staticmethod
def render_licenses(packages_licenses):
licenses = set([pkg_li.get('license') for pkg_li in packages_licenses])
if "N/A" in licenses:
licenses.remove("N/A")
sorted_licenses = sorted(licenses)
return " ".join(sorted_licenses)


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


def license_report(packages, licenses):
size = get_terminal_size()
def license_report(packages, licenses, json_report=False, bare_report=False):
if json_report:
return JsonReport.render_licenses(packages_licenses=licenses)
elif bare_report:
return BareReport.render_licenses(packages_licenses=licenses)

size = get_terminal_size()
if size.columns >= 80:
return SheetReport.render_licenses(packages, licenses)
return BasicReport.render_licenses(packages, licenses)
2 changes: 1 addition & 1 deletion safety/safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def get_licenses(key, db_mirror, cached, proxy):
key = key if key else os.environ.get("SAFETY_API_KEY", False)

if not key and not db_mirror:
raise InvalidKeyError("API-KEY not provided.")
raise InvalidKeyError("The API-KEY was not provided.")
if db_mirror:
mirrors = [db_mirror]
else:
Expand Down
1 change: 1 addition & 0 deletions tests/reqs_4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
django==1.11
105 changes: 104 additions & 1 deletion tests/test_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,42 @@ def test_review_fail(self):
result = runner.invoke(cli.cli, ['review', '--bare', '--file', path_to_report])
assert result.exit_code == -1

@patch("safety.safety.get_licenses")
def test_license_bare(self, get_licenses):
runner = CliRunner()

dirname = os.path.dirname(__file__)
with open(os.path.join(dirname, "test_db", "licenses.json")) as f:
licenses_db = json.loads(f.read())
get_licenses.return_value = licenses_db
reqs_path = os.path.join(dirname, "reqs_4.txt")

result = runner.invoke(cli.cli, ['license', '--file', reqs_path, '--bare', '--db', 'licenses.json'])
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.output, 'BSD-3-Clause\n')

@patch("safety.safety.get_licenses")
def test_license_json(self, get_licenses):
runner = CliRunner()

dirname = os.path.dirname(__file__)
with open(os.path.join(dirname, "test_db", "licenses.json")) as f:
licenses_db = json.loads(f.read())
get_licenses.return_value = licenses_db
reqs_path = os.path.join(dirname, "reqs_4.txt")

result = runner.invoke(cli.cli, ['license', '--file', reqs_path, '--json', '--db', 'licenses.json'])
expected_result = json.dumps(
[{
"license": "BSD-3-Clause",
"package": "django",
"version": "1.11"
}],
indent=4, sort_keys=True
)
self.assertEqual(result.exit_code, 0)
self.assertMultiLineEqual(result.output.rstrip(), expected_result)


class TestFormatter(unittest.TestCase):

Expand Down Expand Up @@ -269,7 +305,7 @@ def test_get_packages_licenses_without_api_key(self):
key=None
)
db_generic_exception = error.exception
self.assertEqual(str(db_generic_exception), 'API-KEY not provided.')
self.assertEqual(str(db_generic_exception), 'The API-KEY was not provided.')

@patch("safety.safety.requests")
def test_get_packages_licenses_with_invalid_api_key(self, requests):
Expand Down Expand Up @@ -387,6 +423,73 @@ def test_get_cached_packages_licenses(self, requests):
self.assertNotEqual(resp, licenses_db)
self.assertEqual(resp, original_db)

def test_report_licenses_bare(self):
from safety.formatter import license_report

reqs = StringIO("Django==1.8.1\n\rinexistent==1.0.0")
packages = util.read_requirements(reqs)

# Using DB: test.test_db.licenses.json
licenses_db = safety.get_licenses(
db_mirror=os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"test_db"
),
cached=False,
key=None,
proxy={},
)

pkgs_licenses = util.get_packages_licenses(packages, licenses_db)
output_report = license_report(
packages=packages,
licenses=pkgs_licenses,
json_report=False,
bare_report=True
)
self.assertEqual(output_report, "BSD-3-Clause")

def test_report_licenses_json(self):
from safety.formatter import license_report

reqs = StringIO("Django==1.8.1\n\rinexistent==1.0.0")
packages = util.read_requirements(reqs)

# Using DB: test.test_db.licenses.json
licenses_db = safety.get_licenses(
db_mirror=os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"test_db"
),
cached=False,
key=None,
proxy={},
)

pkgs_licenses = util.get_packages_licenses(packages, licenses_db)
output_report = license_report(
packages=packages,
licenses=pkgs_licenses,
json_report=True,
bare_report=False
)

expected_result = json.dumps(
[{
"license": "BSD-3-Clause",
"package": "django",
"version": "1.8.1"
},
{
"license": "N/A",
"package": "inexistent",
"version": "1.0.0"
}],
indent=4, sort_keys=True
)
# Packages without license are reported as "N/A"
self.assertEqual(output_report.rstrip(), expected_result)


class ReadRequirementsTestCase(unittest.TestCase):

Expand Down

0 comments on commit ef50ad8

Please sign in to comment.