Files
sovereign_ai/SovereignAI-Starter/scripts/generate_flutter_license_triage.py

166 lines
6.2 KiB
Python

"""Create a hash-backed, non-legal triage report for unresolved Flutter licenses."""
from __future__ import annotations
import argparse
import json
import re
from datetime import UTC, datetime
from pathlib import Path
try:
from scripts.generate_component_inventory import (
digest,
flutter_license_file,
package_roots,
parse_lockfile,
)
except ModuleNotFoundError: # direct execution as `python scripts/...py`
from generate_component_inventory import (
digest,
flutter_license_file,
package_roots,
parse_lockfile,
)
def license_signatures(text: str) -> dict[str, str]:
"""Return conservative SPDX-like signatures found anywhere in the file.
Suggestions are for human triage. They do not establish provenance, package
applicability, exceptions, or permission to redistribute.
"""
normalized = re.sub(r"(?m)^\s*(?://|#|\*)\s?", "", text).lower()
normalized = re.sub(r"\s+", " ", normalized)
matches: dict[str, str] = {}
if "mozilla public license version 2.0" in normalized:
matches["MPL-2.0"] = "Mozilla Public License 2.0 title"
if "apache license" in normalized and "version 2.0" in normalized:
matches["Apache-2.0"] = "Apache License 2.0 title"
if (
"permission is hereby granted, free of charge" in normalized
and "the software is provided" in normalized
and "in no event shall" in normalized
):
matches["MIT"] = "standard MIT grant and warranty disclaimer"
if (
"redistribution and use in source and binary forms" in normalized
and "neither the name" in normalized
and "disclaimer" in normalized
):
matches["BSD-3-Clause"] = "BSD three-clause endorsement restriction"
if (
"redistribution and use in source and binary forms" in normalized
and "neither the name" not in normalized
and "disclaimer" in normalized
and "provided that the following conditions are met" in normalized
):
matches["BSD-2-Clause"] = "BSD two-clause grant and disclaimer"
return matches
def candidate_license(text: str) -> tuple[str | None, str]:
"""Suggest one SPDX identifier only when exactly one signature is present."""
matches = license_signatures(text)
if len(matches) == 1:
license_id, basis = next(iter(matches.items()))
return license_id, basis
if len(matches) > 1:
return None, "multiple license signatures occur in this file; manual review required"
return None, "no conservative license-text signature matched"
def build_report(root: Path) -> dict[str, object]:
app_root = root / "flutter_app"
packages = parse_lockfile(app_root / "pubspec.lock")
roots = package_roots(app_root / ".dart_tool" / "package_config.json")
entries: list[dict[str, object]] = []
missing: list[dict[str, str]] = []
for package in packages:
name = package["name"]
package_root = roots.get(name)
license_file = flutter_license_file(package_root) if package_root else None
if license_file is None:
missing.append({"name": name, "version": package["version"]})
continue
text = license_file.read_text(encoding="utf-8", errors="replace")
candidate, basis = candidate_license(text)
signatures = license_signatures(text)
entries.append(
{
"name": name,
"version": package["version"],
"scope": package.get("dependency", "transitive"),
"license_file": license_file.name,
"license_file_sha256": digest(license_file),
"candidate_spdx": candidate,
"detected_spdx_candidates": sorted(signatures),
"candidate_basis": basis,
"status": (
"single signature candidate; human review required"
if candidate
else "composite or unclassified notice; manual review required"
),
}
)
counts: dict[str, int] = {}
signature_counts: dict[str, int] = {}
multi_signature_files = 0
for entry in entries:
candidate = entry["candidate_spdx"] or "unclassified"
counts[str(candidate)] = counts.get(str(candidate), 0) + 1
detected = entry["detected_spdx_candidates"]
if len(detected) > 1:
multi_signature_files += 1
for license_id in detected:
signature_counts[license_id] = signature_counts.get(license_id, 0) + 1
return {
"format": "flutter-license-triage-v2",
"generated_at": datetime.now(UTC).isoformat(),
"source": "flutter_app/pubspec.lock and resolved package LICENSE files",
"notice": (
"Text-signature suggestions only. Not legal advice, provenance verification, "
"or approval to redistribute. Review each package, source, notices, and terms."
),
"summary": {
"locked_packages": len(packages),
"packages_with_license_file": len(entries),
"packages_without_license_file": len(missing),
"candidate_counts": dict(sorted(counts.items())),
"multi_signature_files": multi_signature_files,
"detected_signature_counts": dict(sorted(signature_counts.items())),
},
"packages": sorted(entries, key=lambda item: str(item["name"]).lower()),
"missing_license_file": sorted(missing, key=lambda item: item["name"].lower()),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--project-root",
type=Path,
default=Path(__file__).resolve().parents[1],
)
parser.add_argument("--output", type=Path, default=None)
args = parser.parse_args()
root = args.project_root.resolve()
output = args.output or root / "sbom" / "flutter-license-triage.json"
report = build_report(root)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(json.dumps(report["summary"], ensure_ascii=False, sort_keys=True))
print(f"Wrote {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())