feat(sbom): add Terraform .terraform.lock.hcl parser; ingest railiance repos
- ingest_sbom.py: parse .terraform.lock.hcl provider blocks (name, version); ecosystem stored as 'other' until terraform added to DB ENUM - Registered railiance-bootstrap + railiance-hosts under railiance domain - railiance-hosts ingested: 2 Terraform providers (hashicorp/template 2.2.0, hetznercloud/hcloud 1.52.0) - railiance-bootstrap: no lockfile (pure Ansible/shell — noted in convention) - sbom-convention_v0.1.md: add Terraform + Ansible rows to lockfile table; update registered repos status table Total SBOM: 422 packages across 2 repos (custodian + railiance-hosts) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,10 @@ dashboard (`/sbom`) provides domain-level and repo-level drill-down.
|
|||||||
| Python | `uv.lock` | Preferred. `requirements.txt` accepted as fallback |
|
| Python | `uv.lock` | Preferred. `requirements.txt` accepted as fallback |
|
||||||
| Node / npm | `package-lock.json` | Preferred. `yarn.lock` accepted |
|
| Node / npm | `package-lock.json` | Preferred. `yarn.lock` accepted |
|
||||||
| Rust | `Cargo.lock` | Auto-detected |
|
| 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* |
|
| Go | `go.sum` | *Not yet parsed — planned* |
|
||||||
| Java / JVM | `gradle.lockfile` / `pom.xml` | *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
|
**Principle:** commit lockfiles to the repo. Lockfiles are the SBOM source
|
||||||
of truth; do not generate them at ingest time.
|
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 |
|
| Repo | Domain | Ecosystems | Last Ingest |
|
||||||
|------|--------|------------|-------------|
|
|------|--------|------------|-------------|
|
||||||
| `the-custodian` | custodian | python, node | 2026-03-01 |
|
| `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.)*
|
*(This table is informational. The live view is at the SBOM dashboard.)*
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
_LOCKFILE_PARSERS = {
|
||||||
"uv.lock": _parse_uv_lock,
|
"uv.lock": _parse_uv_lock,
|
||||||
"requirements.txt": _parse_requirements_txt,
|
"requirements.txt": _parse_requirements_txt,
|
||||||
"package-lock.json": _parse_package_lock_json,
|
"package-lock.json": _parse_package_lock_json,
|
||||||
"yarn.lock": _parse_yarn_lock,
|
"yarn.lock": _parse_yarn_lock,
|
||||||
"Cargo.lock": _parse_cargo_lock,
|
"Cargo.lock": _parse_cargo_lock,
|
||||||
|
".terraform.lock.hcl": _parse_terraform_lock_hcl,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Directories that never contain project-level lockfiles
|
# Directories that never contain project-level lockfiles
|
||||||
|
|||||||
Reference in New Issue
Block a user