## 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
88 lines
3.4 KiB
YAML
88 lines
3.4 KiB
YAML
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: prowler
|
|
namespace: prowler
|
|
spec:
|
|
schedule: "0 3 * * 0" # Sunday 3am
|
|
concurrencyPolicy: Forbid
|
|
jobTemplate:
|
|
spec:
|
|
ttlSecondsAfterFinished: 604800 # Auto-delete after 7 days
|
|
template:
|
|
spec:
|
|
serviceAccountName: prowler
|
|
securityContext:
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
initContainers:
|
|
- name: merge-mutelist
|
|
image: registry.ops.eblu.me/blumeops/prowler:kustomized
|
|
command: ["python3", "-c"]
|
|
args:
|
|
- |
|
|
import yaml, glob, pathlib
|
|
merged = {"Mutelist": {"Accounts": {"*": {"Checks": {}}}}}
|
|
for f in sorted(glob.glob("/mutelist-parts/*.yaml")):
|
|
with open(f) as fh:
|
|
data = yaml.safe_load(fh)
|
|
checks = data.get("Mutelist", {}).get("Accounts", {}).get("*", {}).get("Checks", {})
|
|
merged["Mutelist"]["Accounts"]["*"]["Checks"].update(checks)
|
|
pathlib.Path("/tmp/mutelist").mkdir(exist_ok=True)
|
|
with open("/tmp/mutelist/mutelist.yaml", "w") as fh:
|
|
yaml.dump(merged, fh, default_flow_style=False)
|
|
print(f"Merged {len(merged['Mutelist']['Accounts']['*']['Checks'])} checks from {len(glob.glob('/mutelist-parts/*.yaml'))} files")
|
|
volumeMounts:
|
|
- name: mutelist-parts
|
|
mountPath: /mutelist-parts
|
|
- name: mutelist-merged
|
|
mountPath: /tmp/mutelist
|
|
containers:
|
|
- name: prowler
|
|
image: registry.ops.eblu.me/blumeops/prowler:kustomized
|
|
command: ["/bin/sh", "-c"]
|
|
args:
|
|
- |
|
|
DATEDIR=/reports/prowler/$(date +%Y-%m-%d)
|
|
mkdir -p "$DATEDIR"
|
|
prowler kubernetes \
|
|
--compliance cis_1.11_kubernetes \
|
|
--mutelist-file /tmp/mutelist/mutelist.yaml \
|
|
-z \
|
|
--output-formats html csv json-ocsf \
|
|
--output-directory "$DATEDIR"
|
|
volumeMounts:
|
|
- name: reports
|
|
mountPath: /reports
|
|
- name: mutelist-merged
|
|
mountPath: /tmp/mutelist
|
|
readOnly: true
|
|
- name: var-lib-kubelet
|
|
mountPath: /var/lib/kubelet
|
|
readOnly: true
|
|
- name: etc-kubernetes
|
|
mountPath: /etc/kubernetes
|
|
readOnly: true
|
|
- name: var-lib-etcd
|
|
mountPath: /var/lib/etcd
|
|
readOnly: true
|
|
hostPID: true
|
|
restartPolicy: OnFailure
|
|
volumes:
|
|
- name: reports
|
|
persistentVolumeClaim:
|
|
claimName: prowler-reports
|
|
- name: mutelist-parts
|
|
configMap:
|
|
name: prowler-mutelist
|
|
- name: mutelist-merged
|
|
emptyDir: {}
|
|
- name: var-lib-kubelet
|
|
hostPath:
|
|
path: /var/lib/kubelet
|
|
- name: etc-kubernetes
|
|
hostPath:
|
|
path: /etc/kubernetes
|
|
- name: var-lib-etcd
|
|
hostPath:
|
|
path: /var/lib/etcd
|