blumeops/argocd/manifests/prowler/cronjob-image-scan.yaml
Erich Blume 4059b3d27b Add compensating controls framework and date-based report dirs (#320)
## Summary

- Add `compensating-controls.yaml` tracking 9 named controls that justify suppressed security findings
- Update all Prowler mutelist descriptions with `CC: <id>` references to named controls
- Add `mise run review-compensating-controls` task — surfaces stalest control with all codebase references
- Add [[review-compensating-controls]] how-to doc
- Organize Prowler and Kingfisher reports into `YYYY-MM-DD` subdirectories

### Compensating controls

| ID | Mitigates |
|----|-----------|
| `single-user-cluster` | Image cache abuse, RBAC breadth, system pod privileges |
| `tailscale-network-isolation` | Profiling endpoints, weak TLS, debug ports |
| `local-registry` | AlwaysPullImages gap |
| `sso-gated-admin-tools` | ArgoCD wildcard RBAC |
| `operator-managed-pods` | Tailscale proxy pod security settings |
| `ephemeral-privileged-jobs` | Prowler hostPID exposure |
| `trusted-ci-only` | Forgejo runner DinD |
| `init-container-isolation` | Grafana root init container |
| `observability-stack-audit` | Missing apiserver audit logging |

## Test plan

- [ ] `mise run review-compensating-controls` shows table and references
- [ ] `kubectl kustomize argocd/manifests/prowler/` renders correctly
- [ ] Sync prowler and kingfisher, verify next scan writes to dated subdirectory
- [ ] Grep for `CC:` in mutelist files — every muted finding should have at least one

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #320
2026-03-30 17:44:11 -07:00

73 lines
2.7 KiB
YAML

---
apiVersion: batch/v1
kind: CronJob
metadata:
name: prowler-image-scan
namespace: prowler
spec:
schedule: "0 3 * * 6" # Saturday 3am
concurrencyPolicy: Forbid
jobTemplate:
spec:
ttlSecondsAfterFinished: 604800 # Auto-delete after 7 days
template:
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
initContainers:
# Workaround: Prowler's --registry flag is broken (registry args
# not passed to provider constructor). Generate image list from
# zot catalog API instead.
# See: https://github.com/prowler-cloud/prowler/issues/10457
- name: enumerate-images
image: registry.ops.eblu.me/blumeops/prowler:kustomized
command: ["python3", "-c"]
args:
- |
import json, urllib.request
REGISTRY = "https://registry.ops.eblu.me"
catalog = json.loads(urllib.request.urlopen(f"{REGISTRY}/v2/_catalog").read())
images = []
for repo in catalog["repositories"]:
if not repo.startswith("blumeops/"):
continue
tags = json.loads(urllib.request.urlopen(f"{REGISTRY}/v2/{repo}/tags/list").read())
for tag in tags.get("tags") or []:
images.append(f"registry.ops.eblu.me/{repo}:{tag}")
with open("/shared/images.txt", "w") as f:
f.write("\n".join(images) + "\n")
print(f"Discovered {len(images)} images")
for img in images:
print(img)
volumeMounts:
- name: shared
mountPath: /shared
containers:
- name: prowler
image: registry.ops.eblu.me/blumeops/prowler:kustomized
command: ["/bin/sh", "-c"]
args:
- |
DATEDIR=/reports/prowler-images/$(date +%Y-%m-%d)
mkdir -p "$DATEDIR"
prowler image \
--image-list /shared/images.txt \
-z \
--output-formats html csv json-ocsf \
--output-directory "$DATEDIR"
volumeMounts:
- name: reports
mountPath: /reports
- name: shared
mountPath: /shared
readOnly: true
restartPolicy: OnFailure
volumes:
- name: reports
persistentVolumeClaim:
claimName: prowler-reports
- name: shared
emptyDir: {}