diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/BUILD | 23 | ||||
-rw-r--r-- | tools/checker_demo.py | 32 | ||||
-rw-r--r-- | tools/test_helpers.bzl | 47 | ||||
-rw-r--r-- | tools/write_sbom.py | 117 |
4 files changed, 203 insertions, 16 deletions
diff --git a/tools/BUILD b/tools/BUILD index 03bbac1..2b56a34 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -15,10 +15,20 @@ """License declaration and compliance checking tools.""" package( - default_applicable_licenses = ["//:license"], + default_applicable_licenses = ["//:license", "//:package_info"], default_visibility = ["//visibility:public"], ) +licenses(["notice"]) + +filegroup( + name = "standard_package", + srcs = glob(["**"]), + visibility = ["//distro:__pkg__"], +) + +exports_files(["diff_test.sh"]) + py_binary( name = "checker_demo", srcs = ["checker_demo.py"], @@ -26,10 +36,9 @@ py_binary( visibility = ["//visibility:public"], ) -exports_files(["diff_test.sh"]) - -filegroup( - name = "standard_package", - srcs = glob(["**"]), - visibility = ["//distro:__pkg__"], +py_binary( + name = "write_sbom", + srcs = ["write_sbom.py"], + python_version = "PY3", + visibility = ["//visibility:public"], ) diff --git a/tools/checker_demo.py b/tools/checker_demo.py index 73042aa..6cdf07f 100644 --- a/tools/checker_demo.py +++ b/tools/checker_demo.py @@ -23,14 +23,19 @@ import codecs import json # Conditions allowed for all applications -_ALWAYS_ALLOWED_CONDITIONS = frozenset(['notice', 'permissive', 'unencumberd']) +_ALWAYS_ALLOWED_CONDITIONS = frozenset(['notice', 'permissive', 'unencumbered']) -def _get_licenses(licenses_info): +def _load_license_data(licenses_info): with codecs.open(licenses_info, encoding='utf-8') as licenses_file: return json.loads(licenses_file.read()) +def unique_licenses(licenses): + for target in licenses: + for lic in target.get('licenses') or []: + yield lic + def _do_report(out, licenses): """Produce a report showing the set of licenses being used. @@ -41,11 +46,14 @@ def _do_report(out, licenses): Returns: 0 for no restricted licenses. """ - for lic in licenses: # using strange name lic because license is built-in - rule = lic['rule'] - for kind in lic['license_kinds']: - out.write('= %s\n kind: %s\n' % (rule, kind['target'])) - out.write(' conditions: %s\n' % kind['conditions']) + + for target in unique_licenses(licenses): + for lic in target.get('licenses') or []: + print("lic:", lic) + rule = lic['rule'] + for kind in lic['license_kinds']: + out.write('= %s\n kind: %s\n' % (rule, kind['target'])) + out.write(' conditions: %s\n' % kind['conditions']) def _check_conditions(out, licenses, allowed_conditions): @@ -86,7 +94,7 @@ def _do_copyright_notices(out, licenses): def _do_licenses(out, licenses): - for lic in licenses: + for lic in unique_licenses(licenses): path = lic['license_text'] with codecs.open(path, encoding='utf-8') as license_file: out.write('= %s\n' % path) @@ -107,7 +115,13 @@ def main(): help='check that the dep only includes allowed license conditions') args = parser.parse_args() - licenses = _get_licenses(args.licenses_info) + license_data = _load_license_data(args.licenses_info) + target = license_data[0] # we assume only one target for the demo + + top_level_target = target['top_level_target'] + dependencies = target['dependencies'] + licenses = target['licenses'] + err = 0 with codecs.open(args.report, mode='w', encoding='utf-8') as rpt: _do_report(rpt, licenses) diff --git a/tools/test_helpers.bzl b/tools/test_helpers.bzl index 30f1980..3ffb9b7 100644 --- a/tools/test_helpers.bzl +++ b/tools/test_helpers.bzl @@ -38,3 +38,50 @@ def golden_test( golden, ], ) + +def golden_cmd_test( + name, + cmd, + golden, # Required + toolchains = [], + tools = None, + exec_tools = None, + srcs = [], # Optional + **kwargs): # Rest + """Compares cmd output to golden output, passes if they are identical. + + Args: + name: Name of the build rule. + cmd: The command to run to generate output. + golden: The golden file to be compared. + toolchains: List of toolchains needed to run the command, passed to genrule. + tools: List of tools needed to run the command, passed to genrule. + exec_tools: List of tools needed to run the command, passed to genrule. + srcs: List of sources needed as input to the command, passed to genrule. + **kwargs: Any additional parameters for the generated golden_test. + """ + actual = name + ".output" + + # There are some cases where tools are provided and exec_tools are provided. + # Specifying both in the same genrule, confuses the host vs exec rules, + # which prevents python3 from execution. + if tools and exec_tools: + fail("Only set one: tools or exec_tools. " + + "Setting both confuses python execution mode (host vs exec).") + native.genrule( + name = name + "_output", + srcs = srcs, + outs = [actual], + cmd = cmd + " > '$@'", # Redirect to collect output + toolchains = toolchains, + tools = tools, + exec_tools = exec_tools, + testonly = True, + ) + + golden_test( + name = name, + subject = actual, + golden = golden, + **kwargs + ) diff --git a/tools/write_sbom.py b/tools/write_sbom.py new file mode 100644 index 0000000..18286ab --- /dev/null +++ b/tools/write_sbom.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Proof of concept license checker. + +This is only a demonstration. It will be replaced with other tools. +""" + +import argparse +import codecs +import datetime +import json +import os + + +TOOL = 'https//github.com/bazelbuild/rules_license/tools:write_sbom' + +def _load_package_data(package_info): + with codecs.open(package_info, encoding='utf-8') as inp: + return json.loads(inp.read()) + +def _write_sbom_header(out, package): + header = [ + 'SPDXVersion: SPDX-2.2', + 'DataLicense: CC0-1.0', + 'SPDXID: SPDXRef-DOCUMENT', + 'DocumentName: %s' % package, + # TBD + # 'DocumentNamespace: https://swinslow.net/spdx-examples/example1/hello-v3 + 'Creator: Person: %s' % os.getlogin(), + 'Creator: Tool: %s' % TOOL, + datetime.datetime.utcnow().strftime('Created: %Y-%m-%d-%H:%M:%SZ'), + '', + '##### Package: %s' % package, + ] + out.write('\n'.join(header)) + + + +def _write_sbom(out, packages): + """Produce a basic SBOM + + Args: + out: file object to write to + packages: package metadata. A big blob of JSON. + """ + for p in packages: + name = p.get('package_name') or '<unknown>' + out.write('\n') + out.write('SPDXID: "%s"\n' % name) + out.write(' name: "%s"\n' % name) + if p.get('package_version'): + out.write(' versionInfo: "%s"\n' % p['package_version']) + # IGNORE_COPYRIGHT: Not a copyright notice. It is a variable holding one. + cn = p.get('copyright_notice') + if cn: + out.write(' copyrightText: "%s"\n' % cn) + kinds = p.get('license_kinds') + if kinds: + out.write(' licenseDeclared: "%s"\n' % + ','.join([k['name'] for k in kinds])) + url = p.get('package_url') + if url: + out.write(' downloadLocation: %s\n' % url) + + +def main(): + parser = argparse.ArgumentParser( + description='Demonstraton license compliance checker') + + parser.add_argument('--licenses_info', + help='path to JSON file containing all license data') + parser.add_argument('--out', default='sbom.out', help='SBOM output') + args = parser.parse_args() + + license_data = _load_package_data(args.licenses_info) + target = license_data[0] # we assume only one target for the demo + + top_level_target = target['top_level_target'] + dependencies = target['dependencies'] + # It's not really packages, but this is close proxy for now + licenses = target['licenses'] + package_infos = target['packages'] + + # These are similar dicts, so merge them by package. This is not + # strictly true, as different licenese can appear in the same + # package, but it is good enough for demonstrating the sbom. + + all = {x['bazel_package']: x for x in licenses} + for pi in package_infos: + p = all.get(pi['bazel_package']) + if p: + p.update(pi) + else: + all[pi['bazel_package']] = pi + + err = 0 + with codecs.open(args.out, mode='w', encoding='utf-8') as out: + _write_sbom_header(out, package=top_level_target) + _write_sbom(out, all.values()) + return err + + +if __name__ == '__main__': + main() |