Security scan
Run a dependency and vulnerability scan with the `security-scan` run — gated on PRs and on a weekly schedule, no GHA minutes burned on the scan itself.
Source
This recipe ships both triggers — a typed Effect-TS run
(*.run.ts) and a GitHub Actions workflow
(ci.yml). Use whichever fits;
Action mode is the recommended default.
# Recipe: security / dependency scan on Cloudflare
#
# Use case: dependency and vulnerability scanning that you want on every PR
# *and* on a weekly schedule (to catch newly-disclosed CVEs in code that
# hasn't changed). Offload to the shipped `security-scan` run, which runs
# each selected scanner in its own container in parallel.
#
# Mode: Action mode, fire-and-forget. See specs/04-gha-integration.md.
# Run: security-scan — see specs/02-runs.md#5-security-scan.
name: security-scan
on:
pull_request:
schedule:
- cron: "0 6 * * 1" # every Monday 06:00 UTC
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: openhackersclub/flare-dispatch-action@v1
with:
run: security-scan
endpoint: ${{ vars.FLAREDISPATCH_ENDPOINT }}
hmac-secret: ${{ secrets.FLAREDISPATCH_HMAC }}
inputs: |
{
"repo": "${{ github.repository }}",
"sha": "${{ github.sha }}",
"scanners": ["pnpm-audit", "trivy-fs", "gitleaks"],
"failOn": "high"
}
# The scheduled run uses github.sha of the default branch — the same run
# slug, no extra wiring. Findings land in the check-run summary. // Recipe: security / dependency scan — the `security-scan` Run
//
// The typed Run that ./ci.yml dispatches. Runs each selected scanner in its
// own container, in parallel, and fails the check-run if any scanner exits
// non-zero (each scanner is configured to exit non-zero at/above `failOn`).
//
// The scanners are a heterogeneous list, so the fan-out stays plain
// `Effect.forEach` rather than the count-based `sharded` primitive — but each
// scanner's container + checkout is the `workspace` primitive. See
// specs/03-dsl.md § Primitives.
//
// This is the shipped `security-scan` run, reproduced here so the recipe is
// self-contained. Spec: specs/02-runs.md § 5. DSL: specs/03-dsl.md.
import { Effect, Schema } from "effect";
import { defineRun, step, sandbox, artifact } from "@flare-dispatch/core";
import { workspace } from "@flare-dispatch/core/primitives";
const Scanner = Schema.Literal(
"npm-audit", "pnpm-audit", "cargo-audit", "uv-audit",
"trivy-fs", "grype-fs", "gitleaks",
);
const Input = Schema.Struct({
repo: Schema.String,
sha: Schema.String,
scanners: Schema.Array(Scanner),
failOn: Schema.optional(Schema.Literal("any", "high", "critical")), // default "high"
});
const Output = Schema.Struct({
failed: Schema.Boolean,
scannerResults: Schema.Array(
Schema.Struct({
scanner: Schema.String,
exitCode: Schema.Number,
reportUri: Schema.String,
}),
),
});
export const securityScan = defineRun({
name: "security-scan",
version: "1.0.0",
inputs: Input,
outputs: Output,
limits: { maxDurationSec: 1200, maxConcurrency: 4 },
run: (input) =>
Effect.gen(function* () {
const failOn = input.failOn ?? "high";
const scannerResults = yield* step("scan", () =>
Effect.forEach(
input.scanners,
(scanner) =>
Effect.gen(function* () {
const { container, dir } = yield* workspace({
repo: input.repo,
sha: input.sha,
});
const exec = yield* sandbox.exec({
cwd: dir,
container,
env: { FAIL_ON: failOn },
// `scan` is a thin wrapper in the base image that normalizes
// each scanner's flags and exit codes against FAIL_ON.
command: ["scan", scanner],
});
const reportUri = yield* artifact.upload({
name: `${scanner}-report.json`,
path: exec.logPath,
container,
});
return { scanner, exitCode: exec.exitCode, reportUri };
}),
{ concurrency: 4 },
),
);
const failed = scannerResults.some((r) => r.exitCode !== 0);
return { failed, scannerResults };
}),
});