diff --git a/canon/standards/sbom-convention_v0.1.md b/canon/standards/sbom-convention_v0.1.md index 5b82cc1..890f0c7 100644 --- a/canon/standards/sbom-convention_v0.1.md +++ b/canon/standards/sbom-convention_v0.1.md @@ -34,8 +34,10 @@ dashboard (`/sbom`) provides domain-level and repo-level drill-down. | Python | `uv.lock` | Preferred. `requirements.txt` accepted as fallback | | Node / npm | `package-lock.json` | Preferred. `yarn.lock` accepted | | Rust | `Cargo.lock` | Auto-detected | +| Terraform | `.terraform.lock.hcl` | Provider pins; ecosystem stored as `other` until ENUM extended | | Go | `go.sum` | *Not yet parsed — planned* | | Java / JVM | `gradle.lockfile` / `pom.xml` | *Not yet parsed — planned* | +| Ansible | `requirements.yml` | *Not yet parsed — planned* | **Principle:** commit lockfiles to the repo. Lockfiles are the SBOM source of truth; do not generate them at ingest time. @@ -237,6 +239,8 @@ The SBOM dashboard aggregates across all repos within a domain in the | Repo | Domain | Ecosystems | Last Ingest | |------|--------|------------|-------------| | `the-custodian` | custodian | python, node | 2026-03-01 | +| `railiance-bootstrap` | railiance | — (Ansible + shell, no lockfile) | — | +| `railiance-hosts` | railiance | terraform (2 providers) | 2026-03-01 | *(This table is informational. The live view is at the SBOM dashboard.)* diff --git a/state-hub/scripts/ingest_sbom.py b/state-hub/scripts/ingest_sbom.py index 59ce949..c024ac8 100644 --- a/state-hub/scripts/ingest_sbom.py +++ b/state-hub/scripts/ingest_sbom.py @@ -180,12 +180,47 @@ def _parse_cargo_lock(path: Path) -> list[dict]: ] +def _parse_terraform_lock_hcl(path: Path) -> list[dict]: + """Parse .terraform.lock.hcl — extract Terraform provider name + version.""" + entries = [] + current_name: str | None = None + current_version: str | None = None + + for line in path.read_text().splitlines(): + stripped = line.strip() + # e.g.: provider "registry.terraform.io/hetznercloud/hcloud" { + m = re.match(r'^provider\s+"([^"]+)"\s*\{', stripped) + if m: + # Use full provider address as package_name, short name as display + full = m.group(1) + current_name = full # e.g. "registry.terraform.io/hetznercloud/hcloud" + current_version = None + elif current_name is not None: + vm = re.match(r'version\s*=\s*"([^"]+)"', stripped) + if vm: + current_version = vm.group(1) + elif stripped == "}": + entries.append({ + "package_name": current_name, + "package_version": current_version, + "ecosystem": "other", # "terraform" not yet in ENUM; tracked as other + "license_spdx": None, + "is_direct": True, + "is_dev": False, + }) + current_name = None + current_version = None + + return entries + + _LOCKFILE_PARSERS = { "uv.lock": _parse_uv_lock, "requirements.txt": _parse_requirements_txt, "package-lock.json": _parse_package_lock_json, "yarn.lock": _parse_yarn_lock, "Cargo.lock": _parse_cargo_lock, + ".terraform.lock.hcl": _parse_terraform_lock_hcl, } # Directories that never contain project-level lockfiles