Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
Signed-off-by: Marcela Melara <[email protected]>
  • Loading branch information
marcelamelara committed Jan 24, 2023
0 parents commit caf62c3
Show file tree
Hide file tree
Showing 29 changed files with 1,284 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cli/scai-venv
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Software Supply Chain Attribute Integrity

The Software Supply Chain Attribute Integrity, or SCAI (pronounced "sky"), specification proposes a data
format for capturing functional attribute and integrity information about software artifacts and their supply
chain. SCAI data can be associated with executable binaries, statically- or dynamically-linked libraries,
software packages, container images, software toolchains, and compute environments.

As such, SCAI is intended to be implemented as part of an existing software supply chain attestation
framework by software development tools or services (e.g., builders, CI/CD pipelines, software analysis tools)
seeking to capture more granular information about the attributes and behavior of the software artifacts they
produce. That is, SCAI assumes that implementers will have appropriate processes and tooling in place for
capturing other types of software supply chain metadata, which can be extended to add support for SCAI.

For more details and examples, see the full [specification document]().

## Schema

SCAI provides pluggable [schema](https://github.com/intel-sandbox/mmelara.supply-chain-attribute-integrity/tree/main/schema) to be used in conjunction
with existing software supply chain metadata schema.

Currently supported frameworks:
* [in-toto attestation](https://github.com/in-toto/attestation/tree/main/spec)

## Documentation

All documentation can be found under [docs/](https://github.com/intel-sandbox/mmelara.supply-chain-attribute-integrity/tree/main/docs).

## Usage

The general flow is to first generate one or more Attribute
Assertions and then generate a SCAI Report. The
[examples](https://github.com/intel-sandbox/mmelara.supply-chain-attribute-integrity/tree/main/examples) show
how SCAI metadata is generated in a few different use cases.

Note, that the CLI tools do not current generate **signed**
SCAI Reports.

#### CLI Environment Setup

To run the SCAI CLI tools and examples, the following packages
are required on a minimal Ubuntu system. We assume Ubuntu 20.04.

```
sudo apt install git python3 python3-dev python3-venv virtualenv build-essential
```

Then, set up the Python virtualenv for the SCAI CLI tools.

```
make -C cli
```

#### Basic CLI Invocation

```
cd cli
source scai-venv/bin/activate
```

To generate a basic attribute assertion:
```
./scai-attr-assertion -a <attribute string> -o <output filename> [-e <evidence filename>] [-c <conditions string>]
```

To generate a basic SCAI Report with in-toto Link metadata:
```
./scai-report -i <input artifact filenames> -a <attribute assertion filenames> -c <command to execute as string>
```

For a full list of CLI tool options, invoke with the `-h` option.
43 changes: 43 additions & 0 deletions cli/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2020 Intel Corporation
#
# 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
#
# http://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.

DSTDIR=scai-venv

PY_VERSION=${shell python3 --version | sed 's/Python \(3\.[0-9]\).*/\1/'}
PYTHON_DIR=$(DSTDIR)/lib/python$(PY_VERSION)/site-packages/

all : environment

$(PYTHON_DIR) :
@echo INSTALL SCAI API
python3 -m venv $(DSTDIR)
. $(abspath $(DSTDIR)/bin/activate) && pip install --upgrade pip
. $(abspath $(DSTDIR)/bin/activate) && pip install --upgrade wheel
. $(abspath $(DSTDIR)/bin/activate) && pip install --upgrade in-toto
. $(abspath $(DSTDIR)/bin/activate) && pip install ../python

$(DSTDIR) :
@echo CREATE SCAI VENV DIRECTORY $(DSTDIR)
mkdir -p $(DSTDIR)

environment: $(DSTDIR) $(PYTHON_DIR)

clean:
@echo REMOVE SCAI VENV AND PYTHON LIB DIRS
@rm -rf $(DSTDIR) __pycache__
@cd ../python; rm -rf build dist *.egg-info

.phony : all
.phony : clean
.phony : environment
116 changes: 116 additions & 0 deletions cli/scai-attr-assertion
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env python
# Copyright 2022 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""
<Program Name>
scai-attr-assertion
<Author>
Marcela Melara <[email protected]>
<Copyright>
See LICENSE for licensing information.
<Purpose>
Command-line interface for generating ITE-9 SCAI Attribute Assertions.
"""

import argparse
import subprocess
import json
import os
import attr

from scai.attribute_assertion import AttributeAssertion
from scai.object_reference import ObjectReference
from scai.utility import load_json_file

from securesystemslib.util import get_file_hashes

def Main():
parser = argparse.ArgumentParser(allow_abbrev=False)

parser.add_argument('-a', '--attribute', help='Attribute keyword', type=str, required=True)
parser.add_argument('-c', '--conditions', help='Conditions string (arbitrary JSON encoding)', type=json.loads)
parser.add_argument('-e', '--evidence', help='Filename of json-encoded evidence descriptor', type=str)
parser.add_argument('-t', '--target', help='Filename of target artifact', type=str)
parser.add_argument('--target-name', help='Name for target artifact', type=str)
parser.add_argument('--target-type', help='Type of target artifact', type=str)
parser.add_argument('--target-location', help='Location URI of target artifact', type=str)
parser.add_argument('-o', '--outfile', help='Filename to write out this assertion object', type=str, required=True)
parser.add_argument('--target-dir', help='Directory for searching target files', type=str)
parser.add_argument('--evidence-dir', help='Directory for searching evidence files', type=str)
parser.add_argument('--conditions-dir', help='Directory for searching conditions files', type=str)
parser.add_argument('--out-dir', help='Directory for storing generated files', type=str)
parser.add_argument('--pretty-print', help='Flag to pretty-print all json before storing', action='store_true')

options = parser.parse_args()

# Create assertion dict
assertion_dict = { "attribute": options.attribute }

# Read conditions
if options.conditions:
assertion_dict['conditions'] = options.conditions # FIXME: this should be a file

# Read evidencee
if options.evidence:
evidence_dict = load_json_file(options.evidence, search_path=options.evidence_dir)

# this validates the obj ref format
evidence = ObjectReference.read(evidence_dict)
assertion_dict['evidence'] = evidence.to_dict()

# Create target reference (optional)
if options.target:
target_dict = '.'
if options.target_dict:
target_dict = options.target_dict

target_file = target_dict + '/' + options.target
target_digest_dict = get_file_hashes(target_file)

target_dict = {}
target_dict['name'] = options.target
target_dict['digest'] = target_digest_dict

if options.target_name:
target_dict['name'] = options.target_name

if options.target_location:
target_dict['locationURI'] = options.target_location

if options.target_type:
target_dict['objectType'] = options.target_type

# this validates the obj ref format
target = ObjectReference.read(target_dict)

assertion_dict['target'] = target.to_dict()

# this validates the assertion format
assertion = AttributeAssertion.read(assertion_dict)
assertion_dict = assertion.to_dict()

# Write out the assertions file
out_dir = '.'
if options.out_dir:
out_dir = options.out_dir

outfile = options.outfile
if not outfile.endswith('.json'):
outfile += '.json'

indent = 0
if options.pretty_print:
indent = 4

assertion_file = out_dir + '/' + outfile
with open(assertion_file, 'w+') as afile :
afile.write(json.dumps(assertion_dict, indent=indent))

print('Wrote attribute assertion to %s' % assertion_file)

if __name__ == "__main__":
Main()
139 changes: 139 additions & 0 deletions cli/scai-report
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
# Copyright 2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""
<Program Name>
scai-report
<Author>
Marcela Melara <[email protected]>
<Copyright>
See LICENSE for licensing information.
<Purpose>
Command-line interface for generating ITE-9 SCAI Reports.
"""

import argparse
import subprocess
import json
import os
import attr

from scai.report import Report, SCAI_REPORT_TYPE
from scai.attribute_assertion import AttributeAssertion
from scai.object_reference import ObjectReference
from scai.utility import load_json_file, parse_pem_file

from in_toto.runlib import record_artifacts_as_dict, in_toto_run

def Main():
parser = argparse.ArgumentParser(allow_abbrev=False)

parser.add_argument('-i', '--input-artifacts', type=str, help='Filenames of input artifacts', nargs='+', required=True)
parser.add_argument('-o', '--output-artifacts', type=str, help='Filenames of output artifacts', nargs='+')
parser.add_argument('-a', '--attribute-assertions', help='Filename of JSON files for output artifact attribute assertions', nargs='+', type=str, required=True)
parser.add_argument('-p', '--producer-attributes', help='Filename of JSON files for producer attribute assertions', nargs='+', type=str)
parser.add_argument('-c', '--command', help='Command to invoke the tool', type=str, required=True)
parser.add_argument('--metadata-dir', help='Directory for searching/storing metadata files', type=str)
parser.add_argument('--artifact-dir', help='Directory for searching/storing input/output artifacts', type=str)
parser.add_argument('--pretty-print', help='Flag to pretty-print all json before storing', action='store_true')

options = parser.parse_args()

print('Generating SCAI Attribute Report for operation: %s' % options.command)

# assume all SCAI verifiable objects are in the same location
# still use list for compatibility with file util functions
metadata_dir = '.'
if options.metadata_dir:
metadata_dir = options.metadata_dir

# Execute the command and get the operation metadata
# in in-toto Link format
command = options.command.split(' ')
command_list = [os.path.expanduser(arg) if arg.startswith('~') else arg for arg in command]

intoto_link = in_toto_run('test', options.input_artifacts, options.output_artifacts, command_list, record_streams=True, base_path=options.artifact_dir)
intoto_link_dict = attr.asdict(intoto_link.signed)
intoto_link_dict['_type'] = 'https://in-toto.io/Link/v0.2'

# Generate the producer metadata for the SCAI Report
producer_metadata = {}

# Load the producer attribute assertions
if options.producer_attributes:
producer_assertions_list = []
for a in options.producer_attributes:
assertion_dict = load_json_file(a, metadata_dir)
assertion = AttributeAssertion.read(assertion_dict)
del assertion_dict['_type']
producer_assertions_list.append(assertion_dict)

producer_metadata['attributes'] = producer_assertions_list

# Load the subject attribute assertions
subject_assertions_list = []
for a in options.attribute_assertions:
assertion_dict = load_json_file(a, metadata_dir)
assertion = AttributeAssertion.read(assertion_dict)
subject_assertions_list.append(assertion_dict)

scai_report = Report(subjectAttributes=subject_assertions_list, producer=producer_metadata)
scai_report_dict = attr.asdict(scai_report)
del scai_report_dict['_type']

# Write the SCAI metadata to file
# in-toto attestation format
statement = {} # inner in-toto attesation layer
statement['_type'] = 'https://in-toto.io/Statement/v0.1'

# though in-toto supports bundling multiple subjects
# per statement, we want single-subject statements
for pname, pdigest in intoto_link_dict['products'].items():
subjects = [{'name': pname, 'digest': pdigest}]

statement['subject'] = subjects
statement['predicateType'] = SCAI_REPORT_TYPE
statement['predicate'] = scai_report_dict
statement_json = json.dumps(statement)

report_file = metadata_dir + '/' + pname + '-scai.st'

with open(report_file, 'w+') as rfile :
if options.pretty_print:
rfile.write(json.dumps(statement, indent=4))
else:
rfile.write(json.dumps(statement))

print('Wrote in-toto statement for SCAI predicate: %s' % report_file)

# Generate the ITE-9 in-toto Link statement for the command invocation
link_subjects = []
for pname, pdigest in intoto_link_dict['products'].items():
link_subjects.append({'name': pname, 'digest': pdigest})

del intoto_link_dict['products']

statement['subject'] = link_subjects
statement['predicateType'] = intoto_link_dict['_type']

del intoto_link_dict['_type']

statement['predicate'] = intoto_link_dict
statement_json = json.dumps(statement)

report_file = metadata_dir + '/invocation-intoto.st'

with open(report_file, 'w+') as rfile :
if options.pretty_print:
rfile.write(json.dumps(statement, indent=4))
else:
rfile.write(json.dumps(statement))

print('Wrote in-toto statement for Link predicate: %s' % report_file)

if __name__ == "__main__":
Main()
Loading

0 comments on commit caf62c3

Please sign in to comment.