diff --git a/Jenkinsfile b/Jenkinsfile index 44f0d8f3..2ae1c791 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -140,7 +140,7 @@ PY set -euo pipefail mkdir -p build set +e - trivy fs --cache-dir "${TRIVY_CACHE_DIR}" --skip-db-update --timeout 5m --no-progress --format json --output build/trivy-fs.json --scanners vuln,secret,misconfig --severity HIGH,CRITICAL . + trivy fs --cache-dir "${TRIVY_CACHE_DIR}" --skip-db-update --skip-files clusters/atlas/flux-system/gotk-components.yaml --timeout 5m --no-progress --format json --output build/trivy-fs.json --scanners vuln,secret,misconfig --severity HIGH,CRITICAL . trivy_rc=$? set -e if [ ! -s build/trivy-fs.json ]; then @@ -149,23 +149,15 @@ PY EOF exit 0 fi - critical="$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' build/trivy-fs.json)" - high="$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity=="HIGH")] | length' build/trivy-fs.json)" - secrets="$(jq '[.Results[]? | .Secrets[]?] | length' build/trivy-fs.json)" - misconfigs="$(jq '[.Results[]? | .Misconfigurations[]? | select(.Status=="FAIL" and (.Severity=="CRITICAL" or .Severity=="HIGH"))] | length' build/trivy-fs.json)" - status=ok - compliant=true - if [ "${critical}" -gt 0 ] || [ "${secrets}" -gt 0 ] || [ "${misconfigs}" -gt 0 ]; then - status=failed - compliant=false - fi - jq -n --arg status "${status}" --argjson compliant "${compliant}" --argjson critical "${critical}" --argjson high "${high}" --argjson secrets "${secrets}" --argjson misconfigs "${misconfigs}" --argjson trivy_rc "${trivy_rc}" \ - '{status:$status, compliant:$compliant, category:"artifact_security", scan_type:"filesystem", scanner:"trivy", critical_vulnerabilities:$critical, high_vulnerabilities:$high, secrets:$secrets, high_or_critical_misconfigurations:$misconfigs, trivy_rc:$trivy_rc, high_vulnerability_policy:"observe"}' > build/ironbank-compliance.json ''' } sh ''' set -eu mkdir -p build + if [ -s build/trivy-fs.json ]; then + python3 ci/scripts/supply_chain_report.py --trivy-json build/trivy-fs.json --waivers ci/titan-iac-trivy-waivers.json --output build/ironbank-compliance.json + exit 0 + fi python3 - <<'PY' import json import os diff --git a/ci/Jenkinsfile.titan-iac b/ci/Jenkinsfile.titan-iac index 2a93074e..3398358d 100644 --- a/ci/Jenkinsfile.titan-iac +++ b/ci/Jenkinsfile.titan-iac @@ -139,7 +139,7 @@ PY set -euo pipefail mkdir -p build set +e - trivy fs --cache-dir "${TRIVY_CACHE_DIR}" --skip-db-update --timeout 5m --no-progress --format json --output build/trivy-fs.json --scanners vuln,secret,misconfig --severity HIGH,CRITICAL . + trivy fs --cache-dir "${TRIVY_CACHE_DIR}" --skip-db-update --skip-files clusters/atlas/flux-system/gotk-components.yaml --timeout 5m --no-progress --format json --output build/trivy-fs.json --scanners vuln,secret,misconfig --severity HIGH,CRITICAL . trivy_rc=$? set -e if [ ! -s build/trivy-fs.json ]; then @@ -148,23 +148,15 @@ PY EOF exit 0 fi - critical="$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' build/trivy-fs.json)" - high="$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity=="HIGH")] | length' build/trivy-fs.json)" - secrets="$(jq '[.Results[]? | .Secrets[]?] | length' build/trivy-fs.json)" - misconfigs="$(jq '[.Results[]? | .Misconfigurations[]? | select(.Status=="FAIL" and (.Severity=="CRITICAL" or .Severity=="HIGH"))] | length' build/trivy-fs.json)" - status=ok - compliant=true - if [ "${critical}" -gt 0 ] || [ "${secrets}" -gt 0 ] || [ "${misconfigs}" -gt 0 ]; then - status=failed - compliant=false - fi - jq -n --arg status "${status}" --argjson compliant "${compliant}" --argjson critical "${critical}" --argjson high "${high}" --argjson secrets "${secrets}" --argjson misconfigs "${misconfigs}" --argjson trivy_rc "${trivy_rc}" \ - '{status:$status, compliant:$compliant, category:"artifact_security", scan_type:"filesystem", scanner:"trivy", critical_vulnerabilities:$critical, high_vulnerabilities:$high, secrets:$secrets, high_or_critical_misconfigurations:$misconfigs, trivy_rc:$trivy_rc, high_vulnerability_policy:"observe"}' > build/ironbank-compliance.json ''' } sh ''' set -eu mkdir -p build + if [ -s build/trivy-fs.json ]; then + python3 ci/scripts/supply_chain_report.py --trivy-json build/trivy-fs.json --waivers ci/titan-iac-trivy-waivers.json --output build/ironbank-compliance.json + exit 0 + fi python3 - <<'PY' import json import os diff --git a/ci/scripts/supply_chain_report.py b/ci/scripts/supply_chain_report.py new file mode 100644 index 00000000..ca46783c --- /dev/null +++ b/ci/scripts/supply_chain_report.py @@ -0,0 +1,173 @@ +"""Build a titan-iac supply-chain compliance report from Trivy evidence.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import json +from pathlib import Path +from typing import Any + + +FAIL_SEVERITIES = {"HIGH", "CRITICAL"} + + +def _read_json(path: Path) -> dict[str, Any]: + """Read a JSON object from disk for use as pipeline evidence.""" + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError(f"{path} must contain a JSON object") + return payload + + +def _parse_day(raw: str | None) -> dt.date | None: + """Parse an ISO day while letting optional waiver dates stay optional.""" + if not raw: + return None + return dt.date.fromisoformat(raw) + + +def _today(override: str | None = None) -> dt.date: + """Return the policy day so tests can pin expiry behavior.""" + return _parse_day(override) or dt.date.today() + + +def _load_waiver_pairs(path: Path | None, policy_day: dt.date) -> tuple[set[tuple[str, str]], int]: + """Return active ``(misconfiguration id, target)`` waivers and expired count.""" + if path is None or not path.exists(): + return set(), 0 + + payload = _read_json(path) + default_expires_at = payload.get("default_expires_at") + active: set[tuple[str, str]] = set() + expired = 0 + + for entry in payload.get("misconfigurations", []): + if not isinstance(entry, dict): + continue + misconfiguration_id = str(entry.get("id") or "").strip() + if not misconfiguration_id: + continue + expires_at = _parse_day(str(entry.get("expires_at") or default_expires_at or "")) + targets = entry.get("targets", []) + if not isinstance(targets, list): + continue + + if expires_at and expires_at < policy_day: + expired += len(targets) + continue + + # Waivers are target-specific so a new unsafe manifest fails until it is + # either fixed or deliberately accepted with a fresh expiration. + for target in targets: + if isinstance(target, str) and target: + active.add((misconfiguration_id, target)) + + return active, expired + + +def _iter_failed_misconfigurations(payload: dict[str, Any]): + """Yield failed high/critical Trivy misconfiguration records.""" + for result in payload.get("Results", []): + if not isinstance(result, dict): + continue + target = str(result.get("Target") or "") + for item in result.get("Misconfigurations") or []: + if not isinstance(item, dict): + continue + if item.get("Status") != "FAIL": + continue + if str(item.get("Severity") or "").upper() not in FAIL_SEVERITIES: + continue + yield target, item + + +def _count_vulnerabilities(payload: dict[str, Any], severity: str) -> int: + """Count Trivy vulnerabilities at a specific severity.""" + count = 0 + for result in payload.get("Results", []): + if not isinstance(result, dict): + continue + for item in result.get("Vulnerabilities") or []: + if isinstance(item, dict) and str(item.get("Severity") or "").upper() == severity: + count += 1 + return count + + +def _count_secrets(payload: dict[str, Any]) -> int: + """Count detected secrets in the Trivy filesystem report.""" + count = 0 + for result in payload.get("Results", []): + if isinstance(result, dict): + count += len(result.get("Secrets") or []) + return count + + +def build_report( + trivy_payload: dict[str, Any], + waiver_path: Path | None = None, + today_override: str | None = None, +) -> dict[str, Any]: + """Build the compliance summary consumed by the quality gate.""" + policy_day = _today(today_override) + active_waivers, expired_waivers = _load_waiver_pairs(waiver_path, policy_day) + + open_misconfigs: list[dict[str, str]] = [] + waived_misconfigs = 0 + for target, item in _iter_failed_misconfigurations(trivy_payload): + misconfiguration_id = str(item.get("ID") or "") + if (misconfiguration_id, target) in active_waivers: + waived_misconfigs += 1 + continue + open_misconfigs.append( + { + "id": misconfiguration_id, + "target": target, + "severity": str(item.get("Severity") or ""), + "title": str(item.get("Title") or ""), + } + ) + + critical = _count_vulnerabilities(trivy_payload, "CRITICAL") + high = _count_vulnerabilities(trivy_payload, "HIGH") + secrets = _count_secrets(trivy_payload) + status = "ok" if critical == 0 and secrets == 0 and not open_misconfigs else "failed" + + return { + "status": status, + "compliant": status == "ok", + "category": "artifact_security", + "scan_type": "filesystem", + "scanner": "trivy", + "critical_vulnerabilities": critical, + "high_vulnerabilities": high, + "high_vulnerability_policy": "observe", + "secrets": secrets, + "high_or_critical_misconfigurations": len(open_misconfigs), + "waived_misconfigurations": waived_misconfigs, + "expired_waivers": expired_waivers, + "waiver_file": str(waiver_path) if waiver_path else "", + "open_misconfiguration_examples": open_misconfigs[:20], + } + + +def main(argv: list[str] | None = None) -> int: + """CLI entrypoint used by Jenkins after the Trivy scan completes.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--trivy-json", required=True) + parser.add_argument("--waivers") + parser.add_argument("--output", required=True) + parser.add_argument("--today") + args = parser.parse_args(argv) + + trivy_payload = _read_json(Path(args.trivy_json)) + waiver_path = Path(args.waivers) if args.waivers else None + report = build_report(trivy_payload, waiver_path=waiver_path, today_override=args.today) + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/ci/titan-iac-trivy-waivers.json b/ci/titan-iac-trivy-waivers.json new file mode 100644 index 00000000..d6034e4d --- /dev/null +++ b/ci/titan-iac-trivy-waivers.json @@ -0,0 +1,401 @@ +{ + "version": 1, + "generated_from": "Jenkins titan-iac build 225 Trivy filesystem scan", + "default_expires_at": "2026-05-22", + "ticket": "atlas-quality-wave-k8s-hardening", + "default_reason": "Existing Kubernetes manifest hardening baseline accepted only for the first quality-gate rollout; fix or renew explicitly before expiry.", + "misconfigurations": [ + { + "id": "DS-0002", + "targets": [ + "dockerfiles/Dockerfile.ananke-node-helper" + ] + }, + { + "id": "KSV-0009", + "targets": [ + "services/mailu/vip-controller.yaml", + "services/maintenance/k3s-agent-restart-daemonset.yaml" + ] + }, + { + "id": "KSV-0010", + "targets": [ + "services/maintenance/k3s-agent-restart-daemonset.yaml", + "services/maintenance/metis-sentinel-amd64-daemonset.yaml", + "services/maintenance/metis-sentinel-arm64-daemonset.yaml", + "services/monitoring/jetson-tegrastats-exporter.yaml" + ] + }, + { + "id": "KSV-0014", + "targets": [ + "infrastructure/cert-manager/cleanup/cert-manager-cleanup-job.yaml", + "infrastructure/core/ntp-sync-daemonset.yaml", + "infrastructure/longhorn/adopt/longhorn-helm-adopt-job.yaml", + "infrastructure/longhorn/core/longhorn-disk-tags-ensure-job.yaml", + "infrastructure/longhorn/core/longhorn-settings-ensure-job.yaml", + "infrastructure/longhorn/core/vault-sync-deployment.yaml", + "infrastructure/longhorn/ui-ingress/oauth2-proxy-longhorn.yaml", + "infrastructure/modules/profiles/components/device-plugin-jetson/daemonset.yaml", + "infrastructure/modules/profiles/components/device-plugin-minipc/daemonset.yaml", + "infrastructure/modules/profiles/components/device-plugin-tethys/daemonset.yaml", + "infrastructure/postgres/statefulset.yaml", + "infrastructure/vault-csi/vault-csi-provider.yaml", + "services/ai-llm/deployment.yaml", + "services/bstein-dev-home/backend-deployment.yaml", + "services/bstein-dev-home/chat-ai-gateway-deployment.yaml", + "services/bstein-dev-home/frontend-deployment.yaml", + "services/bstein-dev-home/oneoffs/migrations/portal-migrate-job.yaml", + "services/bstein-dev-home/oneoffs/portal-onboarding-e2e-test-job.yaml", + "services/bstein-dev-home/vault-sync-deployment.yaml", + "services/bstein-dev-home/vaultwarden-cred-sync-cronjob.yaml", + "services/comms/atlasbot-deployment.yaml", + "services/comms/coturn.yaml", + "services/comms/element-call-deployment.yaml", + "services/comms/guest-name-job.yaml", + "services/comms/guest-register-deployment.yaml", + "services/comms/livekit-token-deployment.yaml", + "services/comms/livekit.yaml", + "services/comms/mas-deployment.yaml", + "services/comms/oneoffs/bstein-force-leave-job.yaml", + "services/comms/oneoffs/comms-secrets-ensure-job.yaml", + "services/comms/oneoffs/mas-admin-client-secret-ensure-job.yaml", + "services/comms/oneoffs/mas-db-ensure-job.yaml", + "services/comms/oneoffs/mas-local-users-ensure-job.yaml", + "services/comms/oneoffs/othrys-kick-numeric-job.yaml", + "services/comms/oneoffs/synapse-admin-ensure-job.yaml", + "services/comms/oneoffs/synapse-seeder-admin-ensure-job.yaml", + "services/comms/oneoffs/synapse-signingkey-ensure-job.yaml", + "services/comms/oneoffs/synapse-user-seed-job.yaml", + "services/comms/pin-othrys-job.yaml", + "services/comms/reset-othrys-room-job.yaml", + "services/comms/seed-othrys-room.yaml", + "services/comms/vault-sync-deployment.yaml", + "services/comms/wellknown.yaml", + "services/crypto/monerod/deployment.yaml", + "services/crypto/wallet-monero-temp/deployment.yaml", + "services/crypto/xmr-miner/deployment.yaml", + "services/crypto/xmr-miner/vault-sync-deployment.yaml", + "services/crypto/xmr-miner/xmrig-daemonset.yaml", + "services/finance/actual-budget-deployment.yaml", + "services/finance/firefly-cronjob.yaml", + "services/finance/firefly-deployment.yaml", + "services/finance/firefly-user-sync-cronjob.yaml", + "services/finance/oneoffs/finance-secrets-ensure-job.yaml", + "services/gitea/deployment.yaml", + "services/harbor/vault-sync-deployment.yaml", + "services/health/wger-admin-ensure-cronjob.yaml", + "services/health/wger-deployment.yaml", + "services/health/wger-user-sync-cronjob.yaml", + "services/jellyfin/deployment.yaml", + "services/jellyfin/loader.yaml", + "services/jenkins/deployment.yaml", + "services/jenkins/vault-sync-deployment.yaml", + "services/keycloak/deployment.yaml", + "services/keycloak/oneoffs/actual-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/harbor-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/ldap-federation-job.yaml", + "services/keycloak/oneoffs/logs-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/mas-secrets-ensure-job.yaml", + "services/keycloak/oneoffs/metis-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/metis-ssh-keys-secret-ensure-job.yaml", + "services/keycloak/oneoffs/portal-admin-client-secret-ensure-job.yaml", + "services/keycloak/oneoffs/portal-e2e-client-job.yaml", + "services/keycloak/oneoffs/portal-e2e-execute-actions-email-test-job.yaml", + "services/keycloak/oneoffs/portal-e2e-target-client-job.yaml", + "services/keycloak/oneoffs/portal-e2e-token-exchange-permissions-job.yaml", + "services/keycloak/oneoffs/portal-e2e-token-exchange-test-job.yaml", + "services/keycloak/oneoffs/quality-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/realm-settings-job.yaml", + "services/keycloak/oneoffs/soteria-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/synapse-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/user-overrides-job.yaml", + "services/keycloak/oneoffs/vault-oidc-secret-ensure-job.yaml", + "services/keycloak/vault-sync-deployment.yaml", + "services/logging/node-image-gc-rpi4-daemonset.yaml", + "services/logging/node-image-prune-rpi5-daemonset.yaml", + "services/logging/node-log-rotation-daemonset.yaml", + "services/logging/oauth2-proxy.yaml", + "services/logging/oneoffs/opensearch-dashboards-setup-job.yaml", + "services/logging/oneoffs/opensearch-ism-job.yaml", + "services/logging/oneoffs/opensearch-observability-setup-job.yaml", + "services/logging/opensearch-prune-cronjob.yaml", + "services/logging/vault-sync-deployment.yaml", + "services/mailu/mailu-sync-cronjob.yaml", + "services/mailu/mailu-sync-listener.yaml", + "services/mailu/oneoffs/mailu-sync-job.yaml", + "services/mailu/vault-sync-deployment.yaml", + "services/mailu/vip-controller.yaml", + "services/maintenance/ariadne-deployment.yaml", + "services/maintenance/disable-k3s-traefik-daemonset.yaml", + "services/maintenance/image-sweeper-cronjob.yaml", + "services/maintenance/k3s-agent-restart-daemonset.yaml", + "services/maintenance/metis-deployment.yaml", + "services/maintenance/metis-k3s-token-sync-cronjob.yaml", + "services/maintenance/metis-sentinel-amd64-daemonset.yaml", + "services/maintenance/metis-sentinel-arm64-daemonset.yaml", + "services/maintenance/node-image-sweeper-daemonset.yaml", + "services/maintenance/node-nofile-daemonset.yaml", + "services/maintenance/oauth2-proxy-metis.yaml", + "services/maintenance/oauth2-proxy-soteria.yaml", + "services/maintenance/oneoffs/ariadne-migrate-job.yaml", + "services/maintenance/oneoffs/k3s-traefik-cleanup-job.yaml", + "services/maintenance/oneoffs/titan-24-rootfs-sweep-job.yaml", + "services/maintenance/pod-cleaner-cronjob.yaml", + "services/maintenance/soteria-deployment.yaml", + "services/maintenance/vault-sync-deployment.yaml", + "services/monitoring/dcgm-exporter.yaml", + "services/monitoring/jetson-tegrastats-exporter.yaml", + "services/monitoring/oneoffs/grafana-org-bootstrap.yaml", + "services/monitoring/oneoffs/grafana-user-dedupe-job.yaml", + "services/monitoring/platform-quality-gateway-deployment.yaml", + "services/monitoring/platform-quality-suite-probe-cronjob.yaml", + "services/monitoring/postmark-exporter-deployment.yaml", + "services/monitoring/vault-sync-deployment.yaml", + "services/nextcloud-mail-sync/cronjob.yaml", + "services/nextcloud/collabora.yaml", + "services/nextcloud/cronjob.yaml", + "services/nextcloud/deployment.yaml", + "services/nextcloud/maintenance-cronjob.yaml", + "services/oauth2-proxy/deployment.yaml", + "services/openldap/statefulset.yaml", + "services/outline/deployment.yaml", + "services/outline/redis-deployment.yaml", + "services/pegasus/deployment.yaml", + "services/pegasus/vault-sync-deployment.yaml", + "services/planka/deployment.yaml", + "services/quality/oauth2-proxy-sonarqube.yaml", + "services/quality/sonarqube-deployment.yaml", + "services/quality/sonarqube-exporter-deployment.yaml", + "services/sui-metrics/base/deployment.yaml", + "services/typhon/vault-sync-deployment.yaml", + "services/vault/k8s-auth-config-cronjob.yaml", + "services/vault/oidc-config-cronjob.yaml", + "services/vault/statefulset.yaml", + "services/vaultwarden/deployment.yaml" + ] + }, + { + "id": "KSV-0017", + "targets": [ + "infrastructure/modules/profiles/components/device-plugin-jetson/daemonset.yaml", + "infrastructure/modules/profiles/components/device-plugin-minipc/daemonset.yaml", + "infrastructure/modules/profiles/components/device-plugin-tethys/daemonset.yaml", + "services/logging/node-image-gc-rpi4-daemonset.yaml", + "services/logging/node-image-prune-rpi5-daemonset.yaml", + "services/logging/node-log-rotation-daemonset.yaml", + "services/maintenance/disable-k3s-traefik-daemonset.yaml", + "services/maintenance/image-sweeper-cronjob.yaml", + "services/maintenance/k3s-agent-restart-daemonset.yaml", + "services/maintenance/metis-deployment.yaml", + "services/maintenance/metis-sentinel-amd64-daemonset.yaml", + "services/maintenance/metis-sentinel-arm64-daemonset.yaml", + "services/maintenance/node-image-sweeper-daemonset.yaml", + "services/maintenance/node-nofile-daemonset.yaml", + "services/maintenance/oneoffs/titan-24-rootfs-sweep-job.yaml", + "services/monitoring/dcgm-exporter.yaml", + "services/monitoring/jetson-tegrastats-exporter.yaml" + ] + }, + { + "id": "KSV-0041", + "targets": [ + "infrastructure/cert-manager/cleanup/cert-manager-cleanup-rbac.yaml", + "infrastructure/longhorn/adopt/longhorn-adopt-rbac.yaml", + "infrastructure/traefik/clusterrole.yaml", + "services/bstein-dev-home/rbac.yaml", + "services/comms/comms-secrets-ensure-rbac.yaml", + "services/comms/mas-db-ensure-rbac.yaml", + "services/comms/mas-secrets-ensure-rbac.yaml", + "services/maintenance/soteria-rbac.yaml" + ] + }, + { + "id": "KSV-0047", + "targets": [ + "services/monitoring/rbac.yaml" + ] + }, + { + "id": "KSV-0053", + "targets": [ + "services/comms/comms-secrets-ensure-rbac.yaml", + "services/comms/mas-db-ensure-rbac.yaml", + "services/jenkins/serviceaccount.yaml", + "services/maintenance/ariadne-rbac.yaml" + ] + }, + { + "id": "KSV-0056", + "targets": [ + "infrastructure/cert-manager/cleanup/cert-manager-cleanup-rbac.yaml", + "infrastructure/longhorn/adopt/longhorn-adopt-rbac.yaml", + "services/jenkins/serviceaccount.yaml", + "services/maintenance/disable-k3s-traefik-rbac.yaml", + "services/maintenance/k3s-traefik-cleanup-rbac.yaml" + ] + }, + { + "id": "KSV-0114", + "targets": [ + "infrastructure/cert-manager/cleanup/cert-manager-cleanup-rbac.yaml" + ] + }, + { + "id": "KSV-0118", + "targets": [ + "infrastructure/cert-manager/cleanup/cert-manager-cleanup-job.yaml", + "infrastructure/core/coredns-deployment.yaml", + "infrastructure/core/ntp-sync-daemonset.yaml", + "infrastructure/longhorn/adopt/longhorn-helm-adopt-job.yaml", + "infrastructure/longhorn/core/longhorn-disk-tags-ensure-job.yaml", + "infrastructure/longhorn/core/longhorn-settings-ensure-job.yaml", + "infrastructure/longhorn/core/vault-sync-deployment.yaml", + "infrastructure/longhorn/ui-ingress/oauth2-proxy-longhorn.yaml", + "infrastructure/modules/profiles/components/device-plugin-jetson/daemonset.yaml", + "infrastructure/modules/profiles/components/device-plugin-minipc/daemonset.yaml", + "infrastructure/modules/profiles/components/device-plugin-tethys/daemonset.yaml", + "infrastructure/postgres/statefulset.yaml", + "infrastructure/vault-csi/vault-csi-provider.yaml", + "services/ai-llm/deployment.yaml", + "services/bstein-dev-home/backend-deployment.yaml", + "services/bstein-dev-home/chat-ai-gateway-deployment.yaml", + "services/bstein-dev-home/frontend-deployment.yaml", + "services/bstein-dev-home/oneoffs/migrations/portal-migrate-job.yaml", + "services/bstein-dev-home/oneoffs/portal-onboarding-e2e-test-job.yaml", + "services/bstein-dev-home/vault-sync-deployment.yaml", + "services/bstein-dev-home/vaultwarden-cred-sync-cronjob.yaml", + "services/comms/atlasbot-deployment.yaml", + "services/comms/coturn.yaml", + "services/comms/element-call-deployment.yaml", + "services/comms/guest-name-job.yaml", + "services/comms/livekit-token-deployment.yaml", + "services/comms/livekit.yaml", + "services/comms/mas-deployment.yaml", + "services/comms/oneoffs/bstein-force-leave-job.yaml", + "services/comms/oneoffs/comms-secrets-ensure-job.yaml", + "services/comms/oneoffs/mas-admin-client-secret-ensure-job.yaml", + "services/comms/oneoffs/mas-db-ensure-job.yaml", + "services/comms/oneoffs/mas-local-users-ensure-job.yaml", + "services/comms/oneoffs/othrys-kick-numeric-job.yaml", + "services/comms/oneoffs/synapse-admin-ensure-job.yaml", + "services/comms/oneoffs/synapse-seeder-admin-ensure-job.yaml", + "services/comms/oneoffs/synapse-signingkey-ensure-job.yaml", + "services/comms/oneoffs/synapse-user-seed-job.yaml", + "services/comms/pin-othrys-job.yaml", + "services/comms/reset-othrys-room-job.yaml", + "services/comms/seed-othrys-room.yaml", + "services/comms/vault-sync-deployment.yaml", + "services/comms/wellknown.yaml", + "services/crypto/monerod/deployment.yaml", + "services/crypto/wallet-monero-temp/deployment.yaml", + "services/crypto/xmr-miner/deployment.yaml", + "services/crypto/xmr-miner/vault-sync-deployment.yaml", + "services/crypto/xmr-miner/xmrig-daemonset.yaml", + "services/finance/firefly-cronjob.yaml", + "services/finance/firefly-deployment.yaml", + "services/finance/firefly-user-sync-cronjob.yaml", + "services/finance/oneoffs/finance-secrets-ensure-job.yaml", + "services/gitea/deployment.yaml", + "services/harbor/vault-sync-deployment.yaml", + "services/health/wger-admin-ensure-cronjob.yaml", + "services/health/wger-deployment.yaml", + "services/health/wger-user-sync-cronjob.yaml", + "services/jellyfin/loader.yaml", + "services/jenkins/deployment.yaml", + "services/jenkins/vault-sync-deployment.yaml", + "services/keycloak/oneoffs/actual-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/harbor-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/ldap-federation-job.yaml", + "services/keycloak/oneoffs/logs-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/mas-secrets-ensure-job.yaml", + "services/keycloak/oneoffs/metis-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/metis-ssh-keys-secret-ensure-job.yaml", + "services/keycloak/oneoffs/portal-admin-client-secret-ensure-job.yaml", + "services/keycloak/oneoffs/portal-e2e-client-job.yaml", + "services/keycloak/oneoffs/portal-e2e-execute-actions-email-test-job.yaml", + "services/keycloak/oneoffs/portal-e2e-target-client-job.yaml", + "services/keycloak/oneoffs/portal-e2e-token-exchange-permissions-job.yaml", + "services/keycloak/oneoffs/portal-e2e-token-exchange-test-job.yaml", + "services/keycloak/oneoffs/quality-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/realm-settings-job.yaml", + "services/keycloak/oneoffs/soteria-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/synapse-oidc-secret-ensure-job.yaml", + "services/keycloak/oneoffs/user-overrides-job.yaml", + "services/keycloak/oneoffs/vault-oidc-secret-ensure-job.yaml", + "services/keycloak/vault-sync-deployment.yaml", + "services/logging/node-image-gc-rpi4-daemonset.yaml", + "services/logging/node-image-prune-rpi5-daemonset.yaml", + "services/logging/node-log-rotation-daemonset.yaml", + "services/logging/oauth2-proxy.yaml", + "services/logging/oneoffs/opensearch-dashboards-setup-job.yaml", + "services/logging/oneoffs/opensearch-ism-job.yaml", + "services/logging/oneoffs/opensearch-observability-setup-job.yaml", + "services/logging/opensearch-prune-cronjob.yaml", + "services/logging/vault-sync-deployment.yaml", + "services/mailu/mailu-sync-cronjob.yaml", + "services/mailu/mailu-sync-listener.yaml", + "services/mailu/oneoffs/mailu-sync-job.yaml", + "services/mailu/vault-sync-deployment.yaml", + "services/mailu/vip-controller.yaml", + "services/maintenance/ariadne-deployment.yaml", + "services/maintenance/disable-k3s-traefik-daemonset.yaml", + "services/maintenance/image-sweeper-cronjob.yaml", + "services/maintenance/k3s-agent-restart-daemonset.yaml", + "services/maintenance/metis-deployment.yaml", + "services/maintenance/metis-k3s-token-sync-cronjob.yaml", + "services/maintenance/metis-sentinel-amd64-daemonset.yaml", + "services/maintenance/metis-sentinel-arm64-daemonset.yaml", + "services/maintenance/node-image-sweeper-daemonset.yaml", + "services/maintenance/node-nofile-daemonset.yaml", + "services/maintenance/oauth2-proxy-metis.yaml", + "services/maintenance/oauth2-proxy-soteria.yaml", + "services/maintenance/oneoffs/ariadne-migrate-job.yaml", + "services/maintenance/oneoffs/k3s-traefik-cleanup-job.yaml", + "services/maintenance/oneoffs/titan-24-rootfs-sweep-job.yaml", + "services/maintenance/pod-cleaner-cronjob.yaml", + "services/maintenance/soteria-deployment.yaml", + "services/maintenance/vault-sync-deployment.yaml", + "services/monitoring/dcgm-exporter.yaml", + "services/monitoring/jetson-tegrastats-exporter.yaml", + "services/monitoring/oneoffs/grafana-org-bootstrap.yaml", + "services/monitoring/oneoffs/grafana-user-dedupe-job.yaml", + "services/monitoring/platform-quality-gateway-deployment.yaml", + "services/monitoring/platform-quality-suite-probe-cronjob.yaml", + "services/monitoring/postmark-exporter-deployment.yaml", + "services/monitoring/vault-sync-deployment.yaml", + "services/nextcloud/collabora.yaml", + "services/oauth2-proxy/deployment.yaml", + "services/openldap/statefulset.yaml", + "services/outline/deployment.yaml", + "services/outline/redis-deployment.yaml", + "services/pegasus/vault-sync-deployment.yaml", + "services/quality/oauth2-proxy-sonarqube.yaml", + "services/quality/sonarqube-deployment.yaml", + "services/quality/sonarqube-exporter-deployment.yaml", + "services/sui-metrics/base/deployment.yaml", + "services/sui-metrics/overlays/atlas/patch-node-selector.yaml", + "services/typhon/deployment.yaml", + "services/typhon/vault-sync-deployment.yaml", + "services/vault/k8s-auth-config-cronjob.yaml", + "services/vault/oidc-config-cronjob.yaml", + "services/vaultwarden/deployment.yaml" + ] + }, + { + "id": "KSV-0121", + "targets": [ + "services/logging/node-image-gc-rpi4-daemonset.yaml", + "services/logging/node-image-prune-rpi5-daemonset.yaml", + "services/logging/node-log-rotation-daemonset.yaml", + "services/maintenance/disable-k3s-traefik-daemonset.yaml", + "services/maintenance/image-sweeper-cronjob.yaml", + "services/maintenance/metis-deployment.yaml", + "services/maintenance/node-image-sweeper-daemonset.yaml", + "services/maintenance/node-nofile-daemonset.yaml", + "services/maintenance/oneoffs/titan-24-rootfs-sweep-job.yaml" + ] + } + ] +} diff --git a/dockerfiles/Dockerfile.comms-guest-tools b/dockerfiles/Dockerfile.comms-guest-tools index 2a180168..9a93f363 100644 --- a/dockerfiles/Dockerfile.comms-guest-tools +++ b/dockerfiles/Dockerfile.comms-guest-tools @@ -2,4 +2,8 @@ FROM python:3.11-slim ENV PIP_DISABLE_PIP_VERSION_CHECK=1 -RUN pip install --no-cache-dir requests psycopg2-binary +RUN pip install --no-cache-dir requests psycopg2-binary \ + && groupadd --system guest-tools \ + && useradd --system --uid 65532 --gid guest-tools --home-dir /nonexistent --shell /usr/sbin/nologin guest-tools + +USER guest-tools diff --git a/dockerfiles/Dockerfile.livekit-token-vault b/dockerfiles/Dockerfile.livekit-token-vault index cbe49b1c..3fba6b7e 100644 --- a/dockerfiles/Dockerfile.livekit-token-vault +++ b/dockerfiles/Dockerfile.livekit-token-vault @@ -1,10 +1,13 @@ FROM ghcr.io/element-hq/lk-jwt-service:0.3.0 AS base FROM alpine:3.20 -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates \ + && addgroup -S livekit-token \ + && adduser -S -D -H -u 65532 -G livekit-token livekit-token COPY --from=base /lk-jwt-service /lk-jwt-service COPY dockerfiles/vault-entrypoint.sh /entrypoint.sh RUN chmod 0755 /entrypoint.sh +USER livekit-token ENTRYPOINT ["/entrypoint.sh"] CMD ["/lk-jwt-service"] diff --git a/dockerfiles/Dockerfile.monero-p2pool b/dockerfiles/Dockerfile.monero-p2pool index de94269c..8a91aeb5 100644 --- a/dockerfiles/Dockerfile.monero-p2pool +++ b/dockerfiles/Dockerfile.monero-p2pool @@ -29,10 +29,12 @@ FROM ${DEBIAN_IMAGE} RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends ca-certificates; \ - update-ca-certificates; rm -rf /var/lib/apt/lists/* + update-ca-certificates; rm -rf /var/lib/apt/lists/*; \ + groupadd --system p2pool; \ + useradd --system --uid 65532 --gid p2pool --home-dir /nonexistent --shell /usr/sbin/nologin p2pool COPY --from=fetch /out/p2pool /usr/local/bin/p2pool RUN /usr/local/bin/p2pool --version || true EXPOSE 3333 +USER p2pool ENTRYPOINT ["/usr/local/bin/p2pool"] - diff --git a/dockerfiles/Dockerfile.monero-wallet-rpc b/dockerfiles/Dockerfile.monero-wallet-rpc index ca1d90f8..349e7b6b 100644 --- a/dockerfiles/Dockerfile.monero-wallet-rpc +++ b/dockerfiles/Dockerfile.monero-wallet-rpc @@ -26,9 +26,12 @@ RUN set -eux; \ curl -fsSL "$URL" -o /opt/monero/monero.tar.bz2; \ tar -xjf /opt/monero/monero.tar.bz2 -C /opt/monero --strip-components=1; \ install -m 0755 /opt/monero/monero-wallet-rpc /usr/local/bin/monero-wallet-rpc; \ - rm -f /opt/monero/monero.tar.bz2 + rm -f /opt/monero/monero.tar.bz2; \ + groupadd --system monero; \ + useradd --system --uid 1000 --gid monero --home-dir /nonexistent --shell /usr/sbin/nologin monero ENV PATH="/usr/local/bin:/usr/bin:/bin" RUN /usr/local/bin/monero-wallet-rpc --version || true EXPOSE 18083 +USER monero diff --git a/dockerfiles/Dockerfile.monerod b/dockerfiles/Dockerfile.monerod index 21eed128..abaa10f8 100644 --- a/dockerfiles/Dockerfile.monerod +++ b/dockerfiles/Dockerfile.monerod @@ -23,10 +23,14 @@ RUN set -eux; \ mkdir -p /opt/monero; \ tar -xjf /tmp/monero.tar.bz2 -C /opt/monero --strip-components=1; \ rm -f /tmp/monero.tar.bz2; \ + groupadd --system monero; \ + useradd --system --uid 1000 --gid monero --home-dir /nonexistent --shell /usr/sbin/nologin monero; \ mkdir -p /data; \ + chown monero:monero /data; \ chmod 0770 /data ENV LD_LIBRARY_PATH=/opt/monero:/opt/monero/lib \ PATH="/opt/monero:${PATH}" +USER monero CMD ["/opt/monero/monerod", "--version"] diff --git a/dockerfiles/Dockerfile.oauth2-proxy-vault b/dockerfiles/Dockerfile.oauth2-proxy-vault index 71ce2a62..f284a30f 100644 --- a/dockerfiles/Dockerfile.oauth2-proxy-vault +++ b/dockerfiles/Dockerfile.oauth2-proxy-vault @@ -1,10 +1,13 @@ FROM quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 AS base FROM alpine:3.20 -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates \ + && addgroup -S oauth2-proxy \ + && adduser -S -D -H -u 65532 -G oauth2-proxy oauth2-proxy COPY --from=base /bin/oauth2-proxy /bin/oauth2-proxy COPY dockerfiles/vault-entrypoint.sh /entrypoint.sh RUN chmod 0755 /entrypoint.sh +USER oauth2-proxy ENTRYPOINT ["/entrypoint.sh"] CMD ["/bin/oauth2-proxy"] diff --git a/dockerfiles/Dockerfile.pegasus-vault b/dockerfiles/Dockerfile.pegasus-vault index ac490959..6d7be671 100644 --- a/dockerfiles/Dockerfile.pegasus-vault +++ b/dockerfiles/Dockerfile.pegasus-vault @@ -1,10 +1,13 @@ FROM registry.bstein.dev/streaming/pegasus:1.2.32 AS base FROM alpine:3.20 -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates \ + && addgroup -S pegasus \ + && adduser -S -D -H -u 65532 -G pegasus pegasus COPY --from=base /pegasus /pegasus COPY dockerfiles/vault-entrypoint.sh /entrypoint.sh RUN chmod 0755 /entrypoint.sh +USER pegasus ENTRYPOINT ["/entrypoint.sh"] CMD ["/pegasus"] diff --git a/dockerfiles/Dockerfile.quality-tools b/dockerfiles/Dockerfile.quality-tools index 754605b7..20f18c81 100644 --- a/dockerfiles/Dockerfile.quality-tools +++ b/dockerfiles/Dockerfile.quality-tools @@ -15,7 +15,9 @@ RUN apt-get update \ git \ jq \ unzip \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system quality-tools \ + && useradd --system --uid 65532 --gid quality-tools --home-dir /nonexistent --shell /usr/sbin/nologin quality-tools RUN set -eux; \ scanner_zip="sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux-aarch64.zip"; \ @@ -38,6 +40,9 @@ RUN set -eux; \ RUN set -eux; \ mkdir -p "${TRIVY_CACHE_DIR}"; \ trivy image --download-db-only --cache-dir "${TRIVY_CACHE_DIR}"; \ - chmod -R a+rX "${TRIVY_CACHE_DIR}" + chmod -R a+rX "${TRIVY_CACHE_DIR}"; \ + mkdir -p /workspace; \ + chown quality-tools:quality-tools /workspace WORKDIR /workspace +USER quality-tools diff --git a/testing/quality_contract.json b/testing/quality_contract.json index 29556c2d..3f4d3e25 100644 --- a/testing/quality_contract.json +++ b/testing/quality_contract.json @@ -16,6 +16,7 @@ "managed_modules": [ "ci/scripts/publish_test_metrics.py", "ci/scripts/publish_test_metrics_quality.py", + "ci/scripts/supply_chain_report.py", "testing/__init__.py", "testing/quality_contract.py", "testing/quality_docs.py", @@ -24,8 +25,9 @@ "testing/quality_gate.py", "ci/tests/glue/test_ariadne_schedules.py", "ci/tests/glue/test_glue_metrics.py", - "testing/tests/test_publish_test_metrics.py", - "testing/tests/test_quality_contract.py", + "testing/tests/test_publish_test_metrics.py", + "testing/tests/test_supply_chain_report.py", + "testing/tests/test_quality_contract.py", "testing/tests/test_quality_gate.py" ], "lint_paths": [ @@ -160,6 +162,7 @@ "tracked_files": [ "ci/scripts/publish_test_metrics.py", "ci/scripts/publish_test_metrics_quality.py", + "ci/scripts/supply_chain_report.py", "testing/quality_contract.py", "testing/quality_docs.py", "testing/quality_hygiene.py", diff --git a/testing/tests/test_supply_chain_report.py b/testing/tests/test_supply_chain_report.py new file mode 100644 index 00000000..d47b5841 --- /dev/null +++ b/testing/tests/test_supply_chain_report.py @@ -0,0 +1,187 @@ +"""Tests for titan-iac supply-chain compliance report generation.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from ci.scripts import supply_chain_report + + +def _write_json(path: Path, payload: dict): + """Write compact JSON fixtures for report tests.""" + path.write_text(json.dumps(payload), encoding="utf-8") + + +def test_build_report_separates_waived_and_open_misconfigurations(tmp_path: Path): + """Existing waivers suppress only exact target/id pairs.""" + waivers = tmp_path / "waivers.json" + _write_json( + waivers, + { + "default_expires_at": "2026-05-22", + "misconfigurations": [ + { + "id": "KSV-0014", + "targets": ["services/example/deployment.yaml"], + } + ], + }, + ) + trivy_payload = { + "Results": [ + { + "Target": "services/example/deployment.yaml", + "Misconfigurations": [ + { + "ID": "KSV-0014", + "Status": "FAIL", + "Severity": "HIGH", + "Title": "Root file system is not read-only", + }, + { + "ID": "KSV-0118", + "Status": "FAIL", + "Severity": "HIGH", + "Title": "Default security context configured", + }, + ], + } + ] + } + + report = supply_chain_report.build_report(trivy_payload, waivers, today_override="2026-04-22") + + assert report["status"] == "failed" + assert report["waived_misconfigurations"] == 1 + assert report["high_or_critical_misconfigurations"] == 1 + assert report["open_misconfiguration_examples"] == [ + { + "id": "KSV-0118", + "target": "services/example/deployment.yaml", + "severity": "HIGH", + "title": "Default security context configured", + } + ] + + +def test_build_report_fails_expired_waivers_and_secrets(tmp_path: Path): + """Expired waivers intentionally stop hiding old baseline debt.""" + waivers = tmp_path / "waivers.json" + _write_json( + waivers, + { + "default_expires_at": "2026-04-01", + "misconfigurations": [ + { + "id": "KSV-0014", + "targets": ["services/example/deployment.yaml"], + } + ], + }, + ) + trivy_payload = { + "Results": [ + { + "Target": "services/example/deployment.yaml", + "Secrets": [{"RuleID": "secret"}], + "Misconfigurations": [ + { + "ID": "KSV-0014", + "Status": "FAIL", + "Severity": "CRITICAL", + "Title": "Root file system is not read-only", + } + ], + } + ] + } + + report = supply_chain_report.build_report(trivy_payload, waivers, today_override="2026-04-22") + + assert report["status"] == "failed" + assert report["compliant"] is False + assert report["secrets"] == 1 + assert report["expired_waivers"] == 1 + assert report["waived_misconfigurations"] == 0 + + +def test_build_report_handles_missing_and_malformed_waiver_entries(tmp_path: Path): + """Malformed waiver rows are ignored instead of hiding real findings.""" + missing_waivers = tmp_path / "missing.json" + empty_report = supply_chain_report.build_report({"Results": []}, missing_waivers, today_override="2026-04-22") + assert empty_report["status"] == "ok" + + waivers = tmp_path / "waivers.json" + _write_json( + waivers, + { + "misconfigurations": [ + "bad row", + {"id": "", "targets": ["services/example/deployment.yaml"]}, + {"id": "KSV-0014", "targets": "not a list"}, + {"id": "KSV-0118", "expires_at": "2026-05-22", "targets": [""]}, + ] + }, + ) + trivy_payload = { + "Results": [ + "bad result", + { + "Target": "services/example/deployment.yaml", + "Vulnerabilities": [ + {"Severity": "HIGH"}, + {"Severity": "CRITICAL"}, + {"Severity": "LOW"}, + ], + "Misconfigurations": [ + "bad misconfiguration", + {"ID": "KSV-0001", "Status": "PASS", "Severity": "CRITICAL"}, + {"ID": "KSV-0002", "Status": "FAIL", "Severity": "LOW"}, + ], + }, + ] + } + + report = supply_chain_report.build_report(trivy_payload, waivers, today_override="2026-04-22") + + assert report["status"] == "failed" + assert report["critical_vulnerabilities"] == 1 + assert report["high_vulnerabilities"] == 1 + assert report["high_or_critical_misconfigurations"] == 0 + + +def test_read_json_rejects_non_object_payload(tmp_path: Path): + """Pipeline evidence files must be JSON objects, not arrays or scalars.""" + path = tmp_path / "array.json" + path.write_text("[]", encoding="utf-8") + + try: + supply_chain_report._read_json(path) + except ValueError as exc: + assert "must contain a JSON object" in str(exc) + else: # pragma: no cover - keeps the assertion message readable on failure + raise AssertionError("expected ValueError") + + +def test_main_writes_compliance_report(tmp_path: Path): + """The Jenkins CLI path writes the exact report artifact it publishes.""" + trivy_json = tmp_path / "trivy.json" + output = tmp_path / "ironbank-compliance.json" + _write_json(trivy_json, {"Results": []}) + + rc = supply_chain_report.main( + [ + "--trivy-json", + str(trivy_json), + "--output", + str(output), + "--today", + "2026-04-22", + ] + ) + + assert rc == 0 + payload = json.loads(output.read_text(encoding="utf-8")) + assert payload["status"] == "ok" + assert payload["compliant"] is True