Home / Stories / Repository scanner
Story · Hygiene Live

Every manifest read, and only the lines that matter changed.

A GitOps repository collects small debts: a container with no resource requests, a pod that runs as root, a missing label, an image pull policy nobody set. None of them is urgent, all of them add up. The scanner reads every YAML file, fixes what a rule can fix, asks a model only about what needs judgement, and hands you small draft MRs that touch nothing else.

Starts fromEvery Kubernetes and Flux YAML file in your configured repositories.
Looks atThe manifest itself, deterministic linters (kube-score, in-house structural rules, Trivy), recent usage metrics for the namespace, the live object's owner.
You getDraft MRs with minimal, comment-preserving diffs, one per concern, bundled when several findings can safely merge together.
01 · Walk the repository

Every YAML file, but only the ones that changed.

Every six hours the scanner lists the YAML files in each configured repository. A file whose content hash has not changed since the last pass is skipped, so a quiet repository costs almost nothing.

System namespaces (kube-system, flux-system, cert-manager) and kinds such as CRDs are excluded by default, and very large files are skipped rather than half-read.

02 · Rules before models

Deterministic linters go first.

Each manifest is run through kube-score, an in-house structural rule pack (Flux API versions, HelmRelease remediation settings, HPA target bands) and Trivy's misconfiguration checks.

A finding a rule can decide is never left to a model to rediscover. Findings already proposed and still open are recognised and not raised again.

03 · Patch what a rule can patch

Known fixes are applied by code, not by prompt.

For a fixed set of rules there is a patcher: image pull policy, read-only root filesystem, running as a non-root user, ephemeral-storage requests, rollout strategy, replicas, and new PodDisruptionBudgets or NetworkPolicies. The patch is made in memory and becomes its own proposal.

A patcher can also refuse. If a container writes to an unbounded local cache, picking an ephemeral-storage cap would get the pod evicted, so the scanner asks you for the size instead of guessing.

04 · Judgement where it is needed

The model sees the patched manifest, and real usage.

Only then does a model read the manifest. It sees the file after the deterministic patches, so it cannot re-propose them, plus the linter findings it should skip and, for workloads, recent CPU and memory usage from your metrics backend.

Each suggestion has to carry a category, a confidence and evidence. Low-confidence suggestions are dropped.

05 · A minimal patch

The model says what to change. Code makes the change.

The model does not rewrite the file. It returns a short list of structured edits: an operation, a YAML path, a value. These are applied by a round-trip YAML editor that touches only that path.

Comments, key order, block scalars and every other document in a multi-document file stay byte-for-byte as they were. An edit that names a file outside the scan is rejected. No cosmetic churn.

06 · Safety checks on the diff

Suspicious diffs are held, not shipped.

Every diff is checked before it can become an MR. One that removes more than half of a file is escalated to high risk and held as NEEDS_REVIEW. One that looks like a truncated file is flagged too.

If the live object behind the file is rendered by a Helm chart, editing the rendered output would fight the chart forever, so the proposal is escalated with a pointer to the HelmRelease values as the right place for the fix.

07 · Bundle what belongs together

One MR per concern, not one per line.

Two additive findings on the same file are merged into one diff. The same additive change across many files (standard labels, say) becomes one MR listing every file, instead of twenty.

Functional changes such as upgrades or API-version migrations are never bundled across environments, so dev, staging and production can still be merged one at a time. Rule-based patches and model suggestions stay in separate MRs.

08 · The proposal

A small, additive diff with the evidence attached.

For orders-api that means one draft MR: resource requests sized from observed usage, and a hardened security context, merged into one file's diff. The full MR is below.

repo scan · acme-deploy · 214 YAML files
  • apps/shop/checkout-web/*.yaml hash unchanged · skipped
  • apps/payments/*.yaml hash unchanged · skipped
  • infrastructure/cert-manager/*.yaml excluded namespace
  • crds/orders.acme.example.yaml excluded kind · CustomResourceDefinition
  • apps/shop/orders-api/deployment_orders-api.yaml changed · analyse
  • apps/shop/cart/deployment_cart.yaml changed · analyse
2 of 214 files changed since the last pass
deterministic pre-pass · deployment_orders-api.yaml
  • kube-score · container-image-pull-policy patcher available
  • kube-score · container-ephemeral-storage-request-and-limit patcher available
  • kube-score · container-resources needs usage data · to the model
  • trivy · runs as root, writable root filesystem context for the model
  • structural rules Flux API versions current · no findings
  • kube-score · pod-probes already open as a proposal · not raised again
patcher · container-image-pull-policyin memory
      containers:
        - name: orders-api
          image: registry.acme.example/shop/orders-api:4.12.0
+         imagePullPolicy: IfNotPresent
patcher · ephemeral-storage · cart
refused: container cart mounts an unbounded emptyDir cache
  a fixed cap could get the pod evicted; recurring refusals
  become an issue asking you for the real size
Patched manifestrule fixes applied
Usage7d CPU / memory
Modeljudgement only
suggestions · shop/orders-api
  • resources · set requests conf 0.86 · p95 CPU 180m, peak memory 410Mi, no requests set
  • security · harden securityContext conf 0.82 · no runAsNonRoot, root FS writable
  • "follow best practices for probes" conf 0.55 · below threshold · dropped
structured edits returned by the model
[ { op: set, path: spec.template.spec.containers[orders-api].resources.requests,
    value: { cpu: 200m, memory: 512Mi } } ]
applied by the round-trip editor
        - name: orders-api
          # pinned by the release job, do not edit by hand
          image: registry.acme.example/shop/orders-api:4.12.0
+         resources:
+           requests:
+             cpu: 200m
+             memory: 512Mi
          env: … untouched, comments kept …
4 lines added · 0 lines reformatted · other documents spliced back verbatim
diff checks before emission
  • orders-api · +11 −0 additive · PENDING
  • billing/configmap_rules.yaml · −212 of 260 lines over 50% removed · risk high · NEEDS_REVIEW
  • search/statefulset.yaml · deletes to end of file looks truncated · flagged
  • monitoring/deployment_exporter.yaml live object is Helm-rendered · edit the HelmRelease values instead · escalated
held proposals wait for a human; nothing risky is opened quietly
bundling · acme-deploy
  • same file · orders-api requests + securityContext · both additive → 1 MR
  • cross file · "Add standard Kubernetes labels" 14 files → 1 MR
  • cross file · Flux apiVersion migration split by env: dev · staging · production → 3 MRs
  • kube-score · image pull policy × 6 files rule lane · 1 MR, separate from model suggestions
31 findingsemitted
6 MRsto review
Draft · Set resource requests and harden securityContext for shop/orders-api
+         resources:
+           requests: { cpu: 200m, memory: 512Mi }
+         securityContext:
+           runAsNonRoot: true
+           allowPrivilegeEscalation: false
+           readOnlyRootFilesystem: true
category:resourcessource:repo_scan:acme-deployrisk:lowxrev::LGTM
↓ the full merge request is below

The merge request you get.

Illustrative: two findings on the same file, bundled into one draft MR because both only add lines.

Draft

Set resource requests and harden securityContext for shop/orders-api

from gitops-ai/set-resource-requests-and-harden-securitycontext-5d2e91ab into main · 1 file · +11
category:resourcessource:repo_scan:acme-deployrisk:lowxrev::LGTM
OverviewEvidenceChangesValidation
Summary

This bundle combines two findings on apps/shop/orders-api/deployment_orders-api.yaml. Both only add lines, so they merge without conflict.

FindingCategoryChange
No resource requests on orders-apiresourcesRequests set from observed usage
Container may run as root with a writable root filesystemsecuritysecurityContext added
Why it matters

Without requests the scheduler packs this pod as if it needed nothing, so it is first to be starved on a busy node. Without a security context a compromised process can write to its own image and run as UID 0.

Observed usage, last 7 days
Metricp95PeakProposed request
CPU180m310m200m
Memory395Mi410Mi512Mi
From the manifest

No resources block and no securityContext on the container. The image declares a non-root user, so runAsNonRoot: true does not change who the process runs as. Writes go to the mounted /tmp volume, not the root filesystem.

No limits are proposed: limits on a latency-sensitive service are a decision for its owners.

       containers:
         - name: orders-api
           # pinned by the release job, do not edit by hand
           image: registry.acme.example/shop/orders-api:4.12.0
+          resources:
+            requests:
+              cpu: 200m
+              memory: 512Mi
+          securityContext:
+            runAsNonRoot: true
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            capabilities:
+              drop: ["ALL"]
+            seccompProfile: { type: RuntimeDefault }
           env:
             - name: LOG_LEVEL
Checked before this MR was opened
  • Diff is purely additive: 11 lines added, 0 removed, comments and key order unchanged.
  • The live Deployment is not rendered by a Helm chart, so the file in git is the right place for the edit.
  • No other open proposal touches this file.
After merge, expect
  • One rolling restart of orders-api.
  • Pods start and pass readiness; if the process tries to write outside /tmp it will fail at start, so watch the first rollout.
  • Scheduler now reserves 200m CPU / 512Mi per replica.
Rollback

Revert this MR.

R
independent reviewer second opinion, no pipeline context

Checked that the image runs as a non-root user and only writes to the mounted volume. Requests sit just above p95. Diff scope matches the title. LGTM.

What keeps it honest.

The model proposes. Code applies the change and decides whether it may leave.

Rules before modelsWhat kube-score or a structural rule can decide is patched by code, and the model never sees those findings as open work.
Surgical edits onlyModel suggestions are applied as path-level edits by a round-trip YAML editor: comments, order and untouched documents are preserved.
Large removals heldA diff that removes more than half a file, or looks truncated, is escalated and held for human review instead of opened as an MR.
Chart owns chart outputA file whose live object is rendered by Helm is not edited directly; the fix is pointed at the HelmRelease values.
Refusing is allowedWhen a safe value cannot be known (a disk cap over an unbounded cache), the scanner asks you instead of guessing.
No duplicate noiseOpen or recently dismissed proposals are not raised again, and you can opt a manifest out with a skip annotation.

Where this stands.

Live
  • Change-aware scanning of every YAML file across configured repositories
  • kube-score and structural-rule patchers, Trivy findings as model context
  • Structured, comment-preserving edits for model suggestions
  • Diff safety, truncation and chart-ownership escalation
  • Same-file and cross-file bundling with per-environment split
In development
  • Deterministic checks that drop model suggestions whose evidence quotes nothing from the manifest
  • Measuring the chart-ownership check across more repository layouts
Roadmap
  • Trivy rules graduating to code patchers, rule by rule, as their merge rate proves them
  • Storage-class and setting drift between sibling environments
← Previous storySecurity posture