activity-core/activity-definitions/weekly-sbom-staleness.md

45 lines
1.3 KiB
Markdown
Raw Normal View History

---
id: weekly-sbom-staleness
name: Weekly SBOM Staleness Check
enabled: true
owner: custodian-agent
governance: custodian
status: active
trigger:
type: cron
cron_expression: "0 9 * * 1"
timezone: Europe/Berlin
misfire_policy: skip
context_sources:
- type: state-hub
query: repo_sbom_status
params:
repos: all
bind_to: context.repos
Fix repo_sbom_status resolver — close ADHOC-2026-06-01-T01 The state-hub resolver was calling GET /sbom/status?repo={slug}, which State Hub does not expose. Real SBOM routes are /sbom/, /sbom/{slug}, /sbom/snapshots/, /sbom/snapshots/{id}, /sbom/ingest/, /sbom/report/licences/. The weekly-sbom-staleness ActivityDefinition was passing params {repos: all} and the resolver was reading params.get("repo_slug", ""), so the URL collapsed to /sbom/status?repo= and 404'd. _fetch_json swallowed the error, the rule context.repos.sbom_age_days > 30 evaluated against {} and never matched, and the weekly SBOM check has been a silent no-op for as long as the route mismatch has existed. Resolver now supports two modes selected by params: - single-repo: {repo_slug: foo} → GET /sbom/{foo}, returns {repo_slug, last_sbom_at, sbom_age_days, has_sbom} - bulk: {repos: all} → GET /repos/, computes per-repo age, returns the worst repo's fields hoisted to the top of the result alongside stale_count, total_count, worst_* fields, and the full per-repo list Never-scanned repos get a 99999 sentinel age so threshold rules treat them as very stale without forcing the rule to special-case None. Hoisting the worst entry to the top preserves the existing rule expression context.repos.sbom_age_days > 30 (and target_repo: context.repos.repo_slug, though that field is a separate interpolation gap tracked as ADHOC-2026-06-01-T02). The integration tests' aspirational per-repo iteration model is left intact. Live validation against State Hub on 2026-06-01: - single: activity-core → 36 days since 2026-04-26 ingest - bulk: 48 repos total, 46 stale (>30d), worst is info-tech-canon (never scanned), rule expression evaluates True Tests: 120 passed, 1 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 03:31:56 +02:00
# Resolver returns a summary keyed off the worst repo so the rule expression
# below can match without comprehensions (the sandboxed evaluator does not
# support them). See _repo_sbom_status in context_resolvers/state_hub.py.
---
# Weekly SBOM Staleness Check
Runs every Monday at 09:00 Berlin time. Checks all tracked repositories for
SBOM staleness and flags any repository whose SBOM is older than 30 days.
```rule
id: flag-stale-sbom
for_each: context.repos.repos
bind_as: repo
condition: 'context.repo.sbom_age_days > 30'
action:
task_template: Run SBOM rescan for {context.repo.repo_slug}
target_repo: context.repo.repo_slug
priority: medium
labels: ["sbom", "security", "automated"]
```
Fix repo_sbom_status resolver — close ADHOC-2026-06-01-T01 The state-hub resolver was calling GET /sbom/status?repo={slug}, which State Hub does not expose. Real SBOM routes are /sbom/, /sbom/{slug}, /sbom/snapshots/, /sbom/snapshots/{id}, /sbom/ingest/, /sbom/report/licences/. The weekly-sbom-staleness ActivityDefinition was passing params {repos: all} and the resolver was reading params.get("repo_slug", ""), so the URL collapsed to /sbom/status?repo= and 404'd. _fetch_json swallowed the error, the rule context.repos.sbom_age_days > 30 evaluated against {} and never matched, and the weekly SBOM check has been a silent no-op for as long as the route mismatch has existed. Resolver now supports two modes selected by params: - single-repo: {repo_slug: foo} → GET /sbom/{foo}, returns {repo_slug, last_sbom_at, sbom_age_days, has_sbom} - bulk: {repos: all} → GET /repos/, computes per-repo age, returns the worst repo's fields hoisted to the top of the result alongside stale_count, total_count, worst_* fields, and the full per-repo list Never-scanned repos get a 99999 sentinel age so threshold rules treat them as very stale without forcing the rule to special-case None. Hoisting the worst entry to the top preserves the existing rule expression context.repos.sbom_age_days > 30 (and target_repo: context.repos.repo_slug, though that field is a separate interpolation gap tracked as ADHOC-2026-06-01-T02). The integration tests' aspirational per-repo iteration model is left intact. Live validation against State Hub on 2026-06-01: - single: activity-core → 36 days since 2026-04-26 ingest - bulk: 48 repos total, 46 stale (>30d), worst is info-tech-canon (never scanned), rule expression evaluates True Tests: 120 passed, 1 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 03:31:56 +02:00
The bulk resolver exposes the per-repo entries under `context.repos.repos`.
The rule uses explicit `for_each` binding so the workflow evaluates the
condition once per repository and emits one task per stale repo. Action fields
may reference the bound item with `context.repo.*`.