Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions .github/workflows/aibom.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
name: AIBOM Drift Gate
name: AIBOM Drift Gate + GitHub Scanner

on:
pull_request:
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:

jobs:
aibom:
drift-gate:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- run: pip install -e .
- run: aibom generate . -o new_aibom.json
- run: aibom generate . -o new_aibom.json --profile ai-bom-like
- run: |
if [ -f .aibom/baseline.json ]; then
aibom diff .aibom/baseline.json new_aibom.json --fail-on new-model,new-tool,new-external-provider
fi
- uses: actions/upload-artifact@v4
with:
name: aibom-pr-scan
path: |
new_aibom.json
new_aibom_ai_profile.json

scheduled-github-scan:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- run: pip install -e .
- name: Run multi-repo GitHub scan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
aibom scan-github \
--repos-file examples/github_repo_samples/repos.txt \
--output-dir github-scan-out \
--profile ai-bom-like \
--max-repos 10 \
--timeout-sec 240
- uses: actions/upload-artifact@v4
with:
name: aibom-github-scan
path: github-scan-out
137 changes: 55 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AIBOM (Living AI Bill of Materials)

Standards-first, CI-native AIBOM generator for Python/LangChain projects with audit evidence bundling, drift detection, and heuristic risk overlay.
Standards-first, CI-native AIBOM generator for Python/LangChain/JS-TS/Java/Go/.NET projects with SPDX/CycloneDX/SARIF/VEX exports, drift gates, and attestation workflows.

## Install

Expand All @@ -9,111 +9,84 @@ pip install -e .
pip install -r requirements.txt
```

## CLI

```bash
aibom --version
aibom --help
```

Commands:
- `aibom generate`
- `aibom validate`
- `aibom export`
- `aibom diff`
- `aibom bundle`
- `aibom risk`

## Usage

### Generate AIBOM
## CLI quickstart

```bash
aibom generate . -o AI_BOM.json
# generation fails fast if JSON Schema validation fails
```

Optional prompt-content collection (default is metadata-only):

```bash
aibom generate . -o AI_BOM.json --include-prompts
```

### Audit mode (end-to-end)

```bash
aibom generate . -o AI_BOM.json --audit-mode --bundle-out evidence.zip
aibom summarize --input AI_BOM.json
```

### Validation

```bash
aibom generate --audit-mode --out AI_BOM.json
aibom validate AI_BOM.json
```
## ai-bom-like compatibility profile

`aibom generate` fails closed before writing output if schema validation fails. Validation errors include JSON-pointer-like paths to the failing field (for example `/metadata/generated_at`).

### Standards Output
AIBOM keeps the canonical `AI_BOM.json` schema stable by default. For ai-bom-style ergonomics, use `--profile ai-bom-like`.

```bash
aibom export --input AI_BOM.json --format spdx-json -o SPDX.json
aibom export --input AI_BOM.json --format cyclonedx-json -o CYCLONEDX.json
aibom export --input AI_BOM.json --format sarif-json -o FINDINGS.sarif.json
aibom export --input AI_BOM.json --format vex-json -o ADVISORIES.vex.json
aibom generate . -o AI_BOM.json --profile ai-bom-like
# writes AI_BOM.json + AI_BOM_ai_profile.json and prints a concise terminal summary
```

Internal → SPDX/CycloneDX mapping (extended):
- `models[].type` -> `packages[].name`
- `models[].model` -> `packages[].versionInfo`
- `tools[].name` -> `packages[].name`
- `datasets[].type` -> `packages[].name`
- `risk_findings[]` -> SPDX package advisory refs / CycloneDX `vulnerabilities[]`
- detector metadata (`scan_findings[].confidence`, `severity`, `source_type`) -> external refs/properties
- model `provenance` + `lineage` -> standards-compatible external refs/properties

### Drift detection

```bash
aibom diff .aibom/baseline.json AI_BOM.json --fail-on new-model,new-tool,new-external-provider
```
This profile adds a companion presentation JSON with:
- executive summary counts
- grouped AI assets
- risk highlights
- provenance/compliance rollup
- detector coverage stats

### Evidence bundle
## GitHub scanner quickstart

```bash
aibom bundle --input AI_BOM.json --out evidence.zip --baseline .aibom/baseline.json
aibom scan-github \
--repo openai/openai-quickstart-python \
--output-dir out \
--profile ai-bom-like
```

Bundle contains:
- `AIBOM.json`
- `SPDX.json`
- `DIFF.json` (if baseline exists)
- `MANIFEST.json` (SHA256s)
- `ENVIRONMENT.json`
- `COMPLIANCE_MAPPING.md`

### Risk summary
Multi-repo scan:

```bash
aibom risk --input AI_BOM.json
aibom scan-github \
--repos-file repos.txt \
--output-dir out \
--max-repos 20 \
--timeout-sec 240 \
--fail-on new-model,new-tool,new-external-provider \
--max-high-risk 0 \
--max-unsupported 0
```

## For Auditors

See [`docs/FOR_AUDITORS.md`](docs/FOR_AUDITORS.md) for verification procedure, manifest validation, and reproducibility notes.
Output layout:
- `out/<owner__repo>/AI_BOM.json`
- `out/<owner__repo>/AI_BOM_ai_profile.json` (when `--profile ai-bom-like`)
- `out/SUMMARY.md`
- `out/summary.json`

## SOC Deployment Guide
## Core commands

See [`docs/SOC_DEPLOYMENT_GUIDE.md`](docs/SOC_DEPLOYMENT_GUIDE.md) for CI/CD integration and drift gate rollout.
- `aibom generate`
- `aibom scan-github`
- `aibom summarize`
- `aibom validate`
- `aibom export`
- `aibom diff`
- `aibom bundle`
- `aibom attest`
- `aibom risk`

## Compliance Mapping
## Compatibility and migration notes

See [`docs/COMPLIANCE_MAPPING.md`](docs/COMPLIANCE_MAPPING.md). This is a starter mapping only, not legal advice.
- `generate`, `validate`, `export`, `diff`, `bundle`, `attest`, and `risk` remain functional and backward compatible.
- New `scan-github` and `summarize` commands are additive.
- ai-bom-like output is opt-in (`--profile ai-bom-like`) to avoid schema-breaking changes to canonical AIBOM consumers.
- `scan-github` returns nonzero when any repo scan errors or configured gates fail, while still producing aggregate summary files for partial failures.

## Example outputs for known repositories
## Documentation

See [`examples/github_repo_samples/README.md`](examples/github_repo_samples/README.md) for sample output files and a script that scans smaller well-known GitHub AI repositories, with direct links to each scanned repo.
- [GitHub scanner guide](docs/GITHUB_SCANNER_GUIDE.md)
- [For auditors](docs/FOR_AUDITORS.md)
- [SOC deployment guide](docs/SOC_DEPLOYMENT_GUIDE.md)
- [Compliance mapping](docs/COMPLIANCE_MAPPING.md)

## LangChain demo
## Examples

See [`examples/langchain_demo/README.md`](examples/langchain_demo/README.md).
- [`examples/github_repo_samples/`](examples/github_repo_samples/)
- [`examples/langchain_demo/`](examples/langchain_demo/)
29 changes: 16 additions & 13 deletions aibom/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
sha256_bytes,
stable_json,
validate_safe_path,
PathSecurityError,
)


Expand Down Expand Up @@ -91,9 +90,7 @@ def _parse_openssl_time(value: str) -> datetime:
def _certificate_sans(cert_path: Path) -> list[str]:
# Validate certificate path before passing to openssl
safe_cert_path = validate_safe_path(cert_path, must_exist=True, must_be_file=True)
ext = _openssl(
["x509", "-in", str(safe_cert_path), "-noout", "-ext", "subjectAltName"]
).stdout
ext = _openssl(["x509", "-in", str(safe_cert_path), "-noout", "-ext", "subjectAltName"]).stdout
return re.findall(r"DNS:([^,\n]+)", ext)


Expand All @@ -106,7 +103,7 @@ def _verify_chain(
) -> None:
# Validate signing certificate path
safe_signing_cert = validate_safe_path(signing_cert, must_exist=True, must_be_file=True)

if not ca_bundle and not trusted_roots:
return

Expand Down Expand Up @@ -215,7 +212,7 @@ def sign_bundle(
safe_bundle_path = validate_safe_path(bundle_path, must_exist=True, must_be_file=True)
safe_signing_key = validate_safe_path(signing_key, must_exist=True, must_be_file=True)
safe_signing_cert = validate_safe_path(signing_cert, must_exist=True, must_be_file=True)

signature_path = signature_path or bundle_path.with_suffix(bundle_path.suffix + ".sig")
provenance_path = provenance_path or bundle_path.with_name("provenance.json")

Expand Down Expand Up @@ -247,7 +244,7 @@ def sign_bundle(
"sha256": sha256_bytes(signature_path.read_bytes()),
"algorithm": "RSA-SHA256",
},
"certificate": _cert_metadata(signing_cert),
"certificate": _cert_metadata(safe_signing_cert),
"policy_evaluation": {
"status": "not_evaluated",
"checks": {},
Expand All @@ -273,13 +270,13 @@ def verify_bundle_signature(
safe_bundle_path = validate_safe_path(bundle_path, must_exist=True, must_be_file=True)
safe_signature_path = validate_safe_path(signature_path, must_exist=True, must_be_file=True)
safe_signing_cert = validate_safe_path(signing_cert, must_exist=True, must_be_file=True)

# Validate optional paths
safe_ca_bundle: Path | None = None
safe_crl_file: Path | None = None
safe_trusted_roots: list[Path] | None = None
safe_provenance_path: Path | None = None

if ca_bundle is not None:
safe_ca_bundle = validate_safe_path(ca_bundle, must_exist=True, must_be_file=True)
if crl_file is not None:
Expand All @@ -289,16 +286,22 @@ def verify_bundle_signature(
validate_safe_path(p, must_exist=True, must_be_file=True) for p in trusted_roots
]
if provenance_path is not None and provenance_path.exists():
safe_provenance_path = validate_safe_path(provenance_path, must_exist=True, must_be_file=True)

safe_provenance_path = validate_safe_path(
provenance_path, must_exist=True, must_be_file=True
)

_enforce_validity_window(safe_signing_cert)
_verify_chain(safe_signing_cert, safe_ca_bundle, safe_trusted_roots, safe_crl_file, revocation_policy)
_verify_chain(
safe_signing_cert, safe_ca_bundle, safe_trusted_roots, safe_crl_file, revocation_policy
)

policy_checks: dict[str, Any] = {
"certificate_validity": {"status": "passed"},
"certificate_chain": {
"status": "passed" if (safe_ca_bundle or safe_trusted_roots) else "skipped",
"reason": "no trust anchors provided" if not (safe_ca_bundle or safe_trusted_roots) else None,
"reason": (
"no trust anchors provided" if not (safe_ca_bundle or safe_trusted_roots) else None
),
},
}

Expand Down
Loading
Loading