hatto
The hatto
is CLI for SBOM policy evaluation.
$ hatto help
heriet <heriet@heriet.info>
CLI for software license policy check.
USAGE:
hatto <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
SUBCOMMANDS:
evaluate evaluate policy
help Print this message or the help of the given subcommand(s)
Mean
The hatto
is derived from Japanese word 法度(HATTO). HATTO means rule of prohibition.
evaluate
hatto evaluate
is evaluate license policy.
$ hatto evaluate --help
hatto-evaluate
evaluate policy
USAGE:
hatto evaluate [OPTIONS] <FILE>
ARGS:
<FILE>
OPTIONS:
-c, --curation <FILE>
-h, --help Print help information
-o, --output <OUTPUT_FORMAT> [default: human] [possible values: human, json]
-p, --policy <FILE>
-t, --source-type <SOURCE_TYPE> [possible values: tsv, spdx-tag, spdx-json, spdx-yaml,
cyclone-dx-json, cyclone-dx-xml]
The evaluate ARGS file is SBOM or tsv. SBOM supports SPDX
or CycloneDX
.
Yet another hatto supports tsv. This tsv file must contain header.
example example.tsv
name version licenses annotations
foo 1.0.1 MIT,Apache-2.0 usage=service
bar 1.1.2 UNKNOWN
These files can generate with any license collection tool. If the license collection tool does not support SBOM, you shoud convert to tsv or SBOM.
And you can configure --policy
and --curation
.
The --policy
file defines license policy that written in python. The policy file must implements def evaluate(material, result)
.
example polocy.py
#!/usr/bin/python
allowed_licenses = [
"Apache-2.0",
"BSD-3-Clause",
"MIT",
"Unlicense",
]
def evaluate(material, result):
for license in material.licenses:
if license not in allowed_licenses:
result.add_error(f"{license} is not allowed")
$ hatto evaluate --policy policy.py example.tsv
OK foo 1.0.1 licenses:["MIT", "Apache-2.0"] annotations:{"usage": "service"}
NG bar 1.1.2 licenses:["UNKNOWN"] annotations:{}
ERROR UNKNOWN is not allowed
Failure: evaluate failed
UNKNOWN
is not allowed on policy.py
. Therefore hatto evaluate
is failed.
You may know bar
true license is BSD-3-Clause
. In such a case you can patch license information by --curation
file. The curation file must implement def curate_material(material)
.
example curation.py
#!/usr/bin/python
def curate_material(material):
if material.name == "bar":
material.licenses = ["BSD-3-Clause"]
$ hatto evaluate --policy policy.py --curation curation.py example.tsv
OK foo 1.0.1 licenses:["MIT", "Apache-2.0"] annotations:{"usage": "service"}
OK bar 1.1.2 licenses:["BSD-3-Clause"] annotations:{}
These allow hatto to perform flexible license policy evaluation on your teams or organizations.
policy
The policy is intended to be written by someone on your team or organization who is considering available licenses.
The policy file must implements def evaluate(material, result)
written by python.
evaluate
The def evaluate(material, result)
is evaluate policy. If the evaluation fails, you call result.add_error(message)
in this function.
example polocy.py
#!/usr/bin/python
allowed_licenses = [
"Apache-2.0",
"BSD-3-Clause",
"MIT",
"Unlicense",
]
def evaluate(material, result):
for license in material.licenses:
if license not in allowed_licenses:
result.add_error(f"{license} is not allowed")
This policy allows Apache-2.0
, BSD-3-Clause
, MIT
and Unlicense
.
The evaluate argment material
is curated Material by curation, and result
is EvaluateResult.
evaluate using annotations
The Material set some kind of annotation, you can evaluate the policy more flexibly. For example, there may be cases where you want to ignore a license that is normally deny due to some special circumstances.
def evaluate(material, result):
if "ignore" in material.annotations:
return
# your policy check
In the above, when ignore
is set on annotation, the evaluation is always ignored.
The rules for granting annotations are free, so you will need to decide the person who created the policy. For example, the above rule of ignore by ignore
annotation should be in your policy document.
As another example, let's take a policy evaluate of the AGPL 3.0 license.
Suppose your team or organization may use AGPL 3.0 software in a service and you want to check whether the composed software is source distributed according to the AGPL 3.0 license.
For example, you can write the following.
def evaluate(material, result):
for license in material.licenses:
if license.startswith("AGPL"):
usage = material.annotations.get("usage", "unknown")
if usage == "unknown":
result.add_error(f"you must set usage for {license} software")
elif usage == "service" and "project-source-distributed" not in material.annotations:
result.add_error(f"you must project-source-distribute on {license} software")
WARNING:
that the above policy does not check for compliance with all AGPL 3.0 license terms by legal perspective. For example, AGPL 3.0 also requires source code distribution when distributing a composed software even if it is not for use in a service.
The method of compliance with the terms of a particular license must be determined by your team or organization.
Even in the above example, your team or organization should decide under what conditions usage
annotation and project-source-distribute
will be granted and with what values.
EvaluateResult
EvaluateResult
is result of evaluate. If EvaluateResult
contains any errors, hatto evaluate is failed. Conversely, EvaluateResult
not contains errors, hatto evaluate is success.
It can also contain warnings. If the EvaluateResult
contains any warnings, hatto evaluate is not failed.
Methods
EvaluateResult.add_errors(message)
The add_errors
is add error to result.
def evaluate(material, result):
result.add_error("Any licences is not allowed")
$ hatto evaluate --policy=policy.py example.tsv
NG foo 1.0.0 licenses:["MIT"] annotations:{}
ERROR Any licences is not allowed
Failure: evaluate failed
EvaluateResult.add_warnings(message)
The add_warnings
is add warning to result.
def evaluate(material, result):
result.add_warning("Any licences is warning")
$ hatto evaluate --policy=policy.py example.tsv
OK foo 1.0.0 licenses:["MIT"] annotations:{}
WARNING Any licences is warning
default policy
If the --policy
option is not set, the following default policy is executed.
The default policy allows at least what is commonly used in some Permissive License. This policy may change in future versions. So, we recommend that it be not use as possible.
The policy for your team or organization should be written by your team or organization.
#!/usr/bin/python
allowed_licenses = [
"Apache-2.0",
"MIT",
"BSD-3-Clause",
"Unlicense",
]
def evaluate(material, result):
for license in material.licenses:
if license not in allowed_licenses:
result.add_error(f"{license} is not allowed")
tests
The policy is python code. So, it can be test in common way of python tests.
example test_polocy.py
from policy import evaluate
import pytest
class Material:
def __init__(self, name="", version="", licenses=[], annotations={}):
self.name = name
self.version = version
self.licenses = licenses
self.annotations = annotations
def update_annotation(self, key, value):
self.annotations[key] = value
class EvaluateResult:
def __init__(self):
self.errors = []
self.warnings = []
def add_error(self, message):
self.errors.append(message)
def add_warning(self, message):
self.warnings.append(message)
@pytest.mark.parameterize("license_name", ["Apache-2.0", "BSD-3-Clause", "MIT", "Unlicense"])
def test_evaluate_allow(license_name):
result = EvaluateResult()
material = Material("foo", "v0.1.0", [license_name])
evaluate()
assert len(result.errors) == 0
@pytest.mark.parameterize("license_name", ["UNKNOWN"])
def test_evaluate_deny(license_name):
result = EvaluateResult()
material = Material("foo", "v0.1.0", [license_name])
evaluate()
assert len(result.errors) == 1
curation
The role of curation is to correct license information. License information collected by license information collection tools is rarely inaccurate and may need to be corrected manually.
The curation is intended to be written by the project owner. The project owner and the person that determining the organization's licensing policy may be different.
The curation file must implements def curate_material(material)
written by python.
curate_material
The def curate_material(material)
is curate material. In this context, curete means to modify the material and license information.
For example, suppose the license of foo package detected MIT by license collection tool. However, there was an error in the collection process and the true license is Apache 2.0.
You can correct this error using curate_material
.
#!/usr/bin/python
def curate_material(material):
if material.name == "foo":
material.licenses = ["Apache-2.0"]
In another case, suppose you had many package that dual licensed MIT and Apache 2.0 in your project.
For reasons of your organizational policy, you may want to specify which license to use.
#!/usr/bin/python
def curate_material(material):
if set(material.licenses) == set(["MIT", "Apache-2.0"]):
material.licenses = ["MIT"]
Material
Instance Variables
name | type | explain |
---|---|---|
name | string | name of material |
version | string | version of material |
licenses | list | list of license name(string). In most cases, license name is expected to specify SPDX license identifier |
annotations | dict | dict of annotation key(string) to value(string) |
Methods
Material.update_annotation(key, value)
The update_annotation
is update annotation by key-value pair.
WARNING:
Currently, it is possible to update annotations dict, but cannot update by annotations dict key. Therefore, this method is temporary provided. This method may be removed in the future.
#!/usr/bin/python
def curate_material(material):
material.annotations["foo"] = "bar" # not updated
material.annotations |= {"hoge": "fuga"} # python 3.9 later
material.update_annotation("x", "y")
print(material.annotations) # {"hoge": "fuga", "x": "y"}