SkillScreen-2B-Markdown / scripts /check_dependencies.py
JerroldH's picture
Release v0.1.0: full-data deployment model, inference entry point and OOF thresholds
b60475f verified
Raw History Blame Contribute Delete
4.44 kB
"""Optional CPU check for exact PyPI pins in requirements*.txt, using OSV."""
from datetime import datetime, timezone
import json
from pathlib import PurePosixPath
import re
from urllib.request import Request, urlopen
OSV_URL = 'https://api.osv.dev/v1/query'
PIN = re.compile(r'([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[A-Za-z0-9_,.-]+\])?==([A-Za-z0-9][A-Za-z0-9.!+_-]*)')
OTHER_MANIFESTS = {'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
'composer.json', 'composer.lock', 'pyproject.toml', 'poetry.lock',
'uv.lock', 'pipfile', 'pipfile.lock', 'dockerfile', 'go.mod', 'cargo.lock'}
def query_osv(name, version):
payload = json.dumps({'package': {'name': name, 'ecosystem': 'PyPI'}, 'version': version}).encode()
request = Request(OSV_URL, data=payload, headers={'Content-Type': 'application/json',
'User-Agent': 'SkillScreen/0.1'})
with urlopen(request, timeout=15) as response:
raw = response.read(4_194_305)
if len(raw) > 4_194_304:
raise ValueError('OSV response exceeded 4 MiB.')
result = json.loads(raw)
if not isinstance(result, dict) or result.get('error') or not isinstance(result.get('vulns', []), list):
raise ValueError('Unexpected OSV response.')
if result.get('next_page_token'):
raise ValueError('OSV response is paginated; this check is incomplete.')
return result.get('vulns', [])
def check_dependencies(row, lookup=None):
lookup = query_osv if lookup is None else lookup
checked, unresolved, findings, errors = [], [], [], []
cache = {}
manifests = []
for item in row['files']:
name = PurePosixPath(item['path']).name.lower()
if not (name.startswith('requirements') and name.endswith('.txt')):
if name in OTHER_MANIFESTS:
unresolved.append({'path': item['path'], 'reason': 'unsupported_manifest'})
continue
manifests.append(item['path'])
for line_number, raw in enumerate(item['text'].splitlines(), 1):
line = re.split(r'\s+#', raw, maxsplit=1)[0].strip()
if not line or line.startswith('#'):
continue
location = {'path': item['path'], 'line': line_number}
match = PIN.fullmatch(line)
if not match:
unresolved.append({**location, 'reason': 'not_an_unconditional_exact_pin'})
continue
package, version = match.groups()
package = re.sub(r'[-_.]+', '-', package).lower()
key = package, version
if key not in cache:
try:
advisories = lookup(*key)
if not isinstance(advisories, list) or any(not isinstance(a, dict) or not a.get('id') for a in advisories):
raise ValueError('Invalid advisory response.')
cache[key] = [a for a in advisories if not a.get('withdrawn')]
except (OSError, ValueError, TimeoutError) as exc:
cache[key] = str(exc)
result = cache[key]
dependency = {**location, 'ecosystem': 'PyPI', 'package': package, 'version': version}
if isinstance(result, str):
errors.append({**dependency, 'reason': result})
continue
checked.append({**dependency, 'advisory_count': len(result)})
for advisory in result:
findings.append({**dependency, 'type': 'affected_dependency_version',
'advisory_id': advisory['id'], 'aliases': advisory.get('aliases', []),
'summary': advisory.get('summary', ''),
'url': 'https://osv.dev/vulnerability/' + advisory['id'],
'advisory_modified': advisory.get('modified')})
status = ('partial' if errors and checked else 'unavailable' if errors else
'partial' if unresolved else 'complete' if manifests else 'not_applicable')
return {'status': status, 'checked_at': datetime.now(timezone.utc).isoformat(),
'scope': 'Exact, unconditional PyPI pins in requirements*.txt; other files and transitive dependencies are not resolved.',
'manifests': manifests, 'checked': checked, 'findings': findings,
'unresolved': unresolved, 'errors': errors}