scan
invocations ADR-0003 §2.2 pins.── WHY THIS MODULE EXISTS ──────────────────────────────────────────────────
The same two gates were written five times by hand: `pacha` (a `dagger -c`
shell pipeline), `pacha-api`, `pacha-site` and `pacha-ops` (three `docker run`
blocks), and the Trivy step inside `pacha-api`'s Dagger module. Measured
2026-09-05, the five copies had already drifted in two ways that no CI could
see, because every one of them was green:
· `pacha` and `pacha-api` pass `--config /repo/.gitleaks.toml`; `pacha-site`
and `pacha-ops` do not — and only the first two HAVE that file. So the
flag was not a divergence, but nothing anywhere enforced the pairing: add
the file to `pacha-site` and its gate would silently keep ignoring it.
· `pacha-ops` pins the image through a `GITLEAKS_VERSION` env var and the
other three write `v8.21.2` as a literal. Four places to change a pin the
ADR requires to be identical in all of them.
── THE INVOCATIONS ARE NOT PARAMETERS ──────────────────────────────────────
`--severity`, `--pkg-types`, `--ignore-unfixed` and gitleaks' flags are
deliberately NOT exposed as arguments. ADR-0003 §2.2: *"que el gate sea
idéntico es lo que lo hace comparable entre proyectos: si se cambia en uno, se
cambia en todos"*. A knob here turns that invariant into a convention, and a
convention is what the drift above already came from. Changing a threshold
means editing this file and cutting a tag, which is exactly the ceremony the
ADR asks for.
── WHY RUNNING THIS IN DAGGER IS NOT A DEVIATION ───────────────────────────
ADR-0003 §4.4 says gitleaks lives in GHA and not in Dagger, because it scans
the git HISTORY, which lives in the runner's checkout and not in the build
context. That reason is about WHERE THE `.git` COMES FROM, not about who
launches the container: the contract
(`org-gitops/docs/daggerverse-ci-contract.md` §4, invariant 3) settles it —
*"running that same image inside a Dagger container satisfies this; it is not
a deviation, and this line exists so nobody reads it as one"*. `pacha` has
been doing exactly this since 2026-08-21, because `arc-bithome` has no Docker
daemon and `docker run` cannot work there at all.
The constraint the ADR really names survives, and this module ENFORCES it
instead of documenting it: `gitleaks` refuses to run on a directory whose
`.git` is missing or shallow. See the guard on that function.
Installation
dagger install github.com/wildbitca/daggerverse/scan@v0.1.0Entrypoint
Return Type
Scan Example
dagger -m github.com/wildbitca/daggerverse/scan@65b66de365f75ca17cb1fe647ffb753c88d4f56f call \
func (m *MyModule) Example() *dagger.Scan {
return dag.
Scan()
}@function
def example() -> dagger.Scan:
return (
dag.scan()
)@func()
example(): Scan {
return dag
.scan()
}Types
Scan 🔗
gitleaks() 🔗
Secret scan over the FULL git history. Fails the build on any finding.
gitleaks detect --source /repo --redact --exit-code 1, plus
--config /repo/.gitleaks.toml when there is one. --redact keeps the
secret out of the logs — a gate that prints what it found publishes it to
everyone with read access to the run. --exit-code 1 is what makes it a
gate and not a report.
── THE source MUST CARRY A COMPLETE .git ───────────────────────────────
This scans commits, not the working tree. The caller must hand over a
directory taken from a fetch-depth: 0 checkout, with .git included (do
not add it to a Dagger exclude, and note that actions/checkout inside a
job-level container: without git falls back to the API tarball and has no
history at all).
A shallow checkout does not fail: gitleaks finds nothing in one commit and exits 0. The gate goes green without having looked at anything, which is strictly worse than not having the gate, because now there is a green tick claiming otherwise. So this function refuses to scan a shallow or git-less directory instead of reporting success on it — that check is the part of ADR-0003 §2.2 that a YAML comment could only ask for politely.
False positives are retired by FINGERPRINT in .gitleaksignore
(commit:file:rule:line), which gitleaks reads from the scanned root — so a
NEW secret in the same file gets a different fingerprint and still stops the
pipeline. Path allowlists in .gitleaks.toml are reserved for
machine-generated files (pbxproj, lockfiles) whose fingerprints would need
regenerating every week.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| source | Directory ! | - | repository to scan, |
| config | String ! | "" | path to a gitleaks config RELATIVE TO |
| minHistoryDepth | Integer ! | 0 | minimum number of commits the history must contain. 0 only requires that the clone is not shallow, which is the check that actually protects the gate; a positive value is a second belt for a repo whose real history is known to be much longer. |
Example
dagger -m github.com/wildbitca/daggerverse/scan@65b66de365f75ca17cb1fe647ffb753c88d4f56f call \
gitleaks --source DIR_PATH --config string --min-history-depth integerfunc (m *MyModule) Example(ctx context.Context, source *dagger.Directory, config string, minHistoryDepth int) string {
return dag.
Scan().
Gitleaks(ctx, source, config, minHistoryDepth)
}@function
async def example(source: dagger.Directory, config: str, minhistorydepth: int) -> str:
return await (
dag.scan()
.gitleaks(source, config, minhistorydepth)
)@func()
async example(source: Directory, config: string, minHistoryDepth: number): Promise<string> {
return dag
.scan()
.gitleaks(source, config, minHistoryDepth)
}trivy() 🔗
Vulnerability scan of a container image, fail-closed BEFORE it is published.
trivy image --input /image.tar --severity CRITICAL,HIGH --ignore-unfixed
--pkg-types library --exit-code 1 --format table — the invocation ADR-0003
§2.2 pins, character for character, on aquasec/trivy:0.58.0.
⚠️ The binary is named explicitly in the exec. Dagger’s withExec does not
use the container’s ENTRYPOINT, so ["image", "--input", …] would run
image as a command and fail with something that reads like a Trivy usage
error rather than a Dagger one. This is already how pacha-api’s module
does it, and the comment there exists for the same reason.
── WHY THE INVOCATION IS SHAPED LIKE THAT ──────────────────────────────────
--pkg-types library scopes the gate to OUR dependencies, the ones a bump
in this repo can actually fix. Base-image OS CVEs (distroless/debian) are
only closed by Google bumping the base, so gating on them is a build stuck
red on something nobody here can move; they are covered post-publish by
Artifact Registry’s Artifact Analysis, which is enabled on both pacha
projects and produces live findings. --ignore-unfixed is the same idea for
the rest: a CVE with no fix available is not an action.
It runs BEFORE the push, against the local tarball, because an image with a critical CVE that already reached the registry can be deployed by anyone.
⚠️ A run is cached on the CONTENT of the image. Scanning the same digest two weeks later replays the old verdict without re-downloading the vulnerability DB, so this gate answers “was this image clean when it was built”, not “is it clean today”. Rescanning what is already published is Artifact Analysis’ job, by design (ADR-0003 §7.2) — no cache-buster is offered here, because a knob the caller must remember to set is a gate that fails open by omission.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| image | Container | - | container to scan; its tarball is produced here. |
| tarball | File | - | a docker-archive tarball, when the caller already has one.
Exactly one of |
Example
dagger -m github.com/wildbitca/daggerverse/scan@65b66de365f75ca17cb1fe647ffb753c88d4f56f call \
trivyfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Scan().
Trivy(ctx)
}@function
async def example() -> str:
return await (
dag.scan()
.trivy()
)@func()
async example(): Promise<string> {
return dag
.scan()
.trivy()
}