Falco integration
The falco attestor exists in rookery but is not compiled into the default cilock binary — cilock attestors list won't show it. To use it, build a custom binary that includes the plugin with rookery-builder. The flows below assume such a build.
Falco is the de-facto open-source runtime-security engine for Kubernetes — it loads a kernel eBPF probe (or the legacy module driver) and fires structured events whenever a rule matches a syscall, container behavior, or Kubernetes audit event. Under CI/lock, the rookery native falco attestor ingests Falco's line-delimited JSON event output and produces a signed in-toto attestation linked to the host environment, the git commit, the literal capture argv, and per-rule + per-priority aggregations.
Unlike SARIF-shaped tools, Falco's output is a stream of events with a stable JSON schema (time, rule, priority, output, output_fields, K8s context). The native attestor parses every event, aggregates them per rule and per priority, and embeds both the raw events and the summary in the same envelope. A release-gate Rego policy can deny on falco.summary.priorities.error > 0, or on a specific rule firing more than zero times, without having to walk every event.
Validated invocation
# Pre-reqs: Falco installed in your cluster (falcosecurity/falco Helm chart),
# kubeconfig pointed at it, ed25519 key at key.pem.
FALCO_CLUSTER_NAME=<your-cluster> cilock run --step falco-capture \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations falco,environment,git \
--enable-archivista=false \
-- sh -c 'kubectl logs daemonset/falco -n <falco-ns> --tail=500 \
| grep "\"rule\"" > falco-events.jsonl'
This is the recipe exercised in tool-falco-events — validated against the dropbox-clone-dev EKS cluster with a deterministic "Read sensitive file untrusted" rule firing (a test pod cat's /etc/shadow).
The sh -c wrapper is necessary because kubectl logs writes to stdout — the shell redirect routes that stream to falco-events.jsonl so the product/v0.3 Merkle tree can hash it. The command-run/v0.1 predicate records the full sh -c argv; this is not the cp antipattern (you're not making a copy of a file written outside CI/lock's view, you're routing a streaming-only tool's stdout into a file). The grep filters out Falco's startup banner so only event lines are captured.
FALCO_CLUSTER_NAME is the only env var the falco attestor reads — it stamps the captured envelope's falco.cluster field so policies can branch on which cluster the events came from. If unset, the field is empty but the attestation still signs.
The falco attestor is available today via rookery-builder --preset all (guide). It will land in the canonical default cilock binary once rookery#147 merges.
What gets captured
| Predicate type | Source |
|---|---|
https://aflock.ai/attestations/environment/v0.1 | host OS, kernel, env vars (sensitive ones obfuscated) |
https://aflock.ai/attestations/git/v0.1 | commit hash, branch, dirty status |
https://aflock.ai/attestations/material/v0.3 | Merkle root over the working tree before the capture |
https://aflock.ai/attestations/command-run/v0.1 | literal sh -c 'kubectl logs … > falco-events.jsonl' argv + exit code |
https://aflock.ai/attestations/product/v0.3 | Merkle root over falco-events.jsonl as a real product file |
https://aflock.ai/attestations/falco/v0.1 | parsed events + per-rule aggregation + priority counts + cluster name |
The falco/v0.1 predicate body has this shape:
{
"events": [ /* every Falco event verbatim: time, rule, priority, output, output_fields, K8s context */ ],
"summary": {
"total_events": 2,
"priorities": { "warning": 2 },
"rule_hits": [
{ "rule": "Read sensitive file untrusted", "count": 2, "highest_priority": "Warning" }
]
},
"cluster": "dropbox-clone-dev",
"source_file": { "path": "falco-events.jsonl", "sha256": "..." }
}
Why this shape
| Antipattern | Correct shape (this example) |
|---|---|
cilock run ... -- bash -c "kubectl logs ... > events.jsonl && cp events.jsonl falco-product.jsonl" | cilock run ... -- sh -c 'kubectl logs ... > falco-events.jsonl' |
command-run.cmd records the bash -c "... && cp ..." chain | command-run.cmd records the single sh -c with the kubectl + redirect; no cp |
| The product is a copy of a file written outside CI/lock's view | The product is falco-events.jsonl as the wrapped shell wrote it during the step |
Three properties matter under the falco attestor: (1) command-run/v0.1.cmd records the real sh -c argv including the kubectl invocation — not a chained shell with a separate cp. (2) The ptrace spy traces the shell + kubectl child processes because CI/lock is sh's direct parent. (3) product/v0.3 captures falco-events.jsonl as written via the single redirect inside the wrapped step, then the falco attestor parses the same file to produce falco/v0.1.
The single sh -c wrapper is the same pattern as hadolint and govulncheck — tools (or in Falco's case, the kubectl logs consumer) that write structured output to stdout. The shell-redirect is the one-shot conversion from stdout to a file the product attestor can hash. The command-run predicate records the full argv; there's no copy of a file written outside CI/lock's view, so this is not the cp antipattern.
Validate it locally
List the predicate types in the captured envelope:
jq -r '.payload' attestation.json | base64 -d | jq '.predicate.attestations | map(.type)'
Expected output:
[
"https://aflock.ai/attestations/environment/v0.1",
"https://aflock.ai/attestations/git/v0.1",
"https://aflock.ai/attestations/material/v0.3",
"https://aflock.ai/attestations/command-run/v0.1",
"https://aflock.ai/attestations/product/v0.3",
"https://aflock.ai/attestations/falco/v0.1"
]
Confirm command-run.cmd carries the literal sh -c argv:
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/command-run/v0.1") | .attestation.cmd'
# ["sh","-c","kubectl logs daemonset/falco -n <ns> --tail=500 | grep \"\\\"rule\\\"\" > falco-events.jsonl"]
Pull the Falco summary from the signed envelope:
jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/falco/v0.1") | .attestation | {total: .summary.total_events, priorities: .summary.priorities, rules: .summary.rule_hits, cluster}'
# {
# "total": 2,
# "priorities": { "warning": 2 },
# "rules": [ { "rule": "Read sensitive file untrusted", "count": 2, "highest_priority": "Warning" } ],
# "cluster": "dropbox-clone-dev"
# }
Notes
- Falco install. The validated example uses the
falcosecurity/falcoHelm chart withdriver.kind=modern-bpf. JSON output is enabled via--set json_output=true --set falco.json_output=true --set falco.json_include_output_property=true. The full install + capture recipe is intool-falco-events/reproduce.sh. - Why
--tail=500. A long-running Falco daemonset's log buffer can be huge.--tail=500keeps the capture deterministic for a single release-gate step; for forensic captures, run without--tailor stream viakubectl logs -fagainst a separate sidecar. FALCO_CLUSTER_NAMEenv var. The attestor reads this single env var to stamp the envelope'sfalco.clusterfield. Set it in CI so policies can branch on cluster (dropbox-clone-devvsprodvsstaging). If unset, the field is empty but the attestation still signs.- Streaming vs windowed capture. This page documents a windowed capture (
kubectl logs --tail=500). For continuous streaming, run Falco's JSON output to a file via thejson_output_filechart option, thencilock run -- cat /var/log/falco.jsonl > events.jsonlfor the capture step. Either way the attestor parses the same line-delimited JSON. - Real-infra validation. The captured envelope in
tool-falco-events/raw/attestation.jsonis from a realdropbox-clone-devEKS cluster, not a synthetic fixture. The "Read sensitive file untrusted" rule firing is from a deliberate trigger pod (kubectl run --image=alpine -- cat /etc/shadow); the capture has no sensitive data because Falco redactsoutput_fieldscontaining real file contents.
FAQ
Does CI/lock support Falco?
Yes. Wrap sh -c 'kubectl logs daemonset/falco -n <ns> --tail=N | grep "\"rule\"" > falco-events.jsonl' with cilock run --attestations falco,environment,git. The native falco attestor parses every event into a https://aflock.ai/attestations/falco/v0.1 predicate with per-rule and per-priority summaries, alongside the standard collection (environment, git, material, command-run, product).
Does this require the canonical cilock binary?
The falco attestor is on presets/all today — build via rookery-builder --preset all and the resulting binary has it. It will land in the canonical default cilock binary once rookery#147 merges. The attestor itself is stable; only the canonical-main registration is pending.
How do I gate a release on Falco priority counts?
Author a Rego policy on the falco/v0.1 predicate's summary.priorities block. Example: deny if priorities.error > 0 or if any of priorities.alert + priorities.critical + priorities.emergency is nonzero. A per-rule gate (e.g. deny on Read sensitive file untrusted firing more than zero times) uses summary.rule_hits[]. Examples in the policy/ directory once the example's policy bundle lands.
Why parse Falco JSON into a custom predicate instead of SARIF?
Falco events have a richer schema than SARIF's locations-and-rules model — every event has K8s context (pod, namespace, container, image), syscall metadata, and free-form output_fields. A SARIF flattening would lose those; the falco/v0.1 predicate preserves them verbatim and adds the per-rule + per-priority summaries policies care about most.
How does this differ from running Falco standalone?
Standalone Falco emits a JSON event stream with no provenance — nothing binds it to a release, a cluster, a capture window, or a policy. CI/lock adds five predicates around the same events: git/v0.1 (the commit), environment/v0.1 (the host running the capture step), material/v0.3 (the working tree), command-run/v0.1 (the exact sh -c argv + exit code), and product/v0.3 (the events file's content hash). The Falco events themselves are unchanged — same JSON, same downstream pipeline — but the surrounding evidence is now signed and policy-checkable.
See also
falcoattestor — the underlying ingestion path- Validated example: tool-falco-events — real EKS-cluster capture + raw events + raw envelope + reproduce script
- Falco project — upstream
- rookery#147 — canonical-main registration PR
- Tools index