Dagger
Search

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.0

Entrypoint

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
NameTypeDefault ValueDescription
sourceDirectory !-

repository to scan, .git included, from a fetch-depth: 0 checkout.

configString !""

path to a gitleaks config RELATIVE TO source. Empty = auto: use .gitleaks.toml if the repo has one, otherwise the built-in rules. An explicit path that does not exist is an error, never a silent fallback to the weaker default ruleset.

minHistoryDepthInteger !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 integer
func (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
NameTypeDefault ValueDescription
imageContainer -

container to scan; its tarball is produced here.

tarballFile -

a docker-archive tarball, when the caller already has one. Exactly one of image or tarball must be given.

Example
dagger -m github.com/wildbitca/daggerverse/scan@65b66de365f75ca17cb1fe647ffb753c88d4f56f call \
 trivy
func (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()
}