blob: 9010041017de2f54105b69e3c476e17f1c6ce977 [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
"""Canonical inventory from independent Apache Maven dependency:tree JSON."""
import datetime
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
spec = importlib.util.spec_from_file_location('snapshot', Path(__file__).with_name('dependency-snapshot.py'))
snapshot = importlib.util.module_from_spec(spec)
spec.loader.exec_module(snapshot)
REPORT = 'dependency-inventory.json'
RESOLVER = 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:tree'
LIMIT = 25 * 1024 * 1024
FIELDS = {'groupId', 'artifactId', 'version', 'type', 'scope', 'classifier', 'optional', 'children'}
SCOPES = {'compile', 'runtime', 'provided', 'system', 'test'}
def packages(tree):
result = {}
pending = [(tree, 0)]
while pending:
node, depth = pending.pop()
if not isinstance(node, dict) or set(node) - FIELDS:
raise ValueError('Invalid inventory node')
for field in ['groupId', 'artifactId', 'version', 'type']:
if not isinstance(node.get(field), str) or not node[field].strip():
raise ValueError('Incomplete inventory coordinates')
if not isinstance(node.get('classifier'), str) or node.get('optional') not in ['true', 'false']:
raise ValueError('Invalid inventory classifier/optional')
scope = node.get('scope')
if (depth == 0 and scope != '') or (depth > 0 and scope not in SCOPES):
raise ValueError('Invalid inventory scope')
children = node.get('children', [])
if not isinstance(children, list):
raise ValueError('Invalid inventory children')
pending.extend((child, depth + 1) for child in children)
if depth == 0:
continue
purl = snapshot.package_url(dict(node, types=[node['type']], classifiers=[node['classifier']]))
value = {'scope': 'development' if scope == 'test' else 'runtime',
'relationship': 'direct' if depth == 1 else 'indirect'}
# Maven may expose the same selected package along more than one path.
previous = result.get(purl)
if previous:
if previous['scope'] == 'runtime':
value['scope'] = 'runtime'
if previous['relationship'] == 'direct':
value['relationship'] = 'direct'
result[purl] = value
return result
def create_inventory(workspace, poms, sha, repository, run_id, run_attempt):
manifests = {}
for pom in poms:
if pom in manifests:
raise ValueError('Duplicate inventory POM')
report = workspace / Path(pom).parent / 'target' / REPORT
if report.is_symlink():
raise ValueError('Invalid inventory report')
with report.open('rb') as stream:
raw = stream.read(LIMIT + 1)
if not raw or len(raw) > LIMIT:
raise ValueError('Invalid inventory report size')
manifests[pom] = packages(json.loads(raw, object_pairs_hook=snapshot.unique_object))
if not manifests or not any(manifests.values()):
raise ValueError('Empty independent dependency inventory')
return {'version': 1, 'sha': sha, 'repository': repository, 'run_id': run_id,
'run_attempt': run_attempt, 'producer': 'inventory', 'resolver': RESOLVER,
'scanned': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z'),
'manifests': manifests}
if __name__ == '__main__':
poms = subprocess.check_output(['git', 'ls-files', '-z', '*pom.xml']).decode().split('\0')
poms = [pom for pom in poms if Path(pom).name == 'pom.xml']
inventory = create_inventory(Path.cwd(), poms, os.environ['GITHUB_SHA'], os.environ['GITHUB_REPOSITORY'],
os.environ['GITHUB_RUN_ID'], os.environ['GITHUB_RUN_ATTEMPT'])
Path(sys.argv[1]).write_text(json.dumps(inventory, indent=2) + '\n')