create-daloy 0.34.2 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +62 -6
  2. package/bin/create-daloy.mjs +344 -25
  3. package/package.json +2 -2
  4. package/sbom.cdx.json +13 -9
  5. package/sbom.spdx.json +5 -5
  6. package/templates/_ci/deno/SECURITY.md +31 -2
  7. package/templates/_ci/deno/_github/CODEOWNERS +2 -0
  8. package/templates/_ci/deno/_github/workflows/container-scan.yml +42 -8
  9. package/templates/_ci/deno/_github/workflows/deploy.yml +183 -0
  10. package/templates/_ci/deno/_github/workflows/opengrep.yml +137 -0
  11. package/templates/_ci/deno/_github/workflows/osv-scan.yml +121 -0
  12. package/templates/_ci/deno/_github/workflows/secret-scan.yml +106 -0
  13. package/templates/_ci/node/SECURITY.md +100 -1
  14. package/templates/_ci/node/_github/CODEOWNERS +3 -0
  15. package/templates/_ci/node/_github/workflows/container-scan.yml +46 -10
  16. package/templates/_ci/node/_github/workflows/dast.yml +177 -0
  17. package/templates/_ci/node/_github/workflows/deploy.yml +94 -0
  18. package/templates/_ci/node/_github/workflows/opengrep.yml +169 -0
  19. package/templates/_ci/node/_github/workflows/osv-scan.yml +135 -0
  20. package/templates/_ci/node/_github/workflows/secret-scan.yml +106 -0
  21. package/templates/bun-basic/AGENTS.md +12 -0
  22. package/templates/bun-basic/_gitignore +5 -0
  23. package/templates/bun-basic/package.json +3 -2
  24. package/templates/cloudflare-worker/AGENTS.md +13 -0
  25. package/templates/cloudflare-worker/_gitignore +9 -0
  26. package/templates/cloudflare-worker/_npmrc +2 -1
  27. package/templates/cloudflare-worker/package.json +3 -2
  28. package/templates/deno-basic/AGENTS.md +12 -0
  29. package/templates/deno-basic/_gitignore +5 -0
  30. package/templates/deno-basic/deno.json +2 -2
  31. package/templates/node-basic/AGENTS.md +12 -0
  32. package/templates/node-basic/_gitignore +5 -0
  33. package/templates/node-basic/_npmrc +2 -2
  34. package/templates/node-basic/package.json +1 -1
  35. package/templates/vercel-edge/AGENTS.md +12 -0
  36. package/templates/vercel-edge/_gitignore +5 -0
  37. package/templates/vercel-edge/_npmrc +2 -1
  38. package/templates/vercel-edge/package.json +3 -2
@@ -0,0 +1,106 @@
1
+ # Secret scanning — gitleaks against PRs, pushes to main, and a daily
2
+ # scheduled sweep of the full git history.
3
+ #
4
+ # Why this exists (Aikido "Secrets Detection: A Practical Guide"
5
+ # https://www.aikido.dev/blog/secret-detection-application-security):
6
+ #
7
+ # GitHub-native push protection only flags provider-shaped secrets at
8
+ # push time. Aikido's guide argues that effective secret detection must
9
+ # also scan the **full git history** (not just the latest snapshot) on
10
+ # a regular cadence, because a single leaked credential anywhere in any
11
+ # commit, branch, or tag should be treated as compromised.
12
+ #
13
+ # This workflow runs `gitleaks` on every PR / push against the working
14
+ # tree and, on a daily schedule, sweeps the entire commit history so
15
+ # long-lived secrets that pre-date push-protection enablement (or
16
+ # secrets that slipped past it via rebased / squashed fork PRs) are
17
+ # still surfaced.
18
+ #
19
+ # Hardening (matches the rest of the scaffolded CI bundle):
20
+ #
21
+ # * No new third-party GitHub Action: the gitleaks binary is fetched
22
+ # from the pinned official release and its SHA-256 is verified
23
+ # before execution.
24
+ # * Top-level `permissions: {}`; the single job opts in to
25
+ # `contents: read`.
26
+ # * `step-security/harden-runner` in audit mode, sudo disabled.
27
+ # * `actions/checkout` with `persist-credentials: false`.
28
+ # * `--redact` keeps any matched value out of the public log.
29
+ #
30
+ # When a finding lands:
31
+ # 1. Rotate the credential immediately.
32
+ # 2. Assess scope (what did the secret access?).
33
+ # 3. Remove it from history if your workflow allows (filter-repo /
34
+ # BFG); otherwise treat the repo + every downstream clone as
35
+ # compromised.
36
+ # 4. Add a `.gitleaksignore` entry only for confirmed false positives.
37
+
38
+ name: Secret scan
39
+
40
+ on:
41
+ pull_request:
42
+ push:
43
+ branches: [main]
44
+ schedule:
45
+ - cron: "21 4 * * *"
46
+ workflow_dispatch:
47
+
48
+ permissions: {}
49
+
50
+ concurrency:
51
+ group: secret-scan-${{ github.workflow }}-${{ github.ref }}
52
+ cancel-in-progress: true
53
+
54
+ jobs:
55
+ gitleaks:
56
+ name: gitleaks
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 10
59
+ permissions:
60
+ contents: read
61
+
62
+ env:
63
+ # Pinned gitleaks release — bump in lockstep with the SHA-256 below.
64
+ # Source of truth: https://github.com/gitleaks/gitleaks/releases
65
+ GITLEAKS_VERSION: "8.30.1"
66
+ GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb"
67
+
68
+ steps:
69
+ - name: Harden runner
70
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
71
+ with:
72
+ egress-policy: audit
73
+ disable-sudo: true
74
+ disable-file-monitoring: false
75
+
76
+ - name: Checkout
77
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
78
+ with:
79
+ persist-credentials: false
80
+ show-progress: false
81
+ # Full history for the scheduled sweep so gitleaks can walk
82
+ # every commit; for PR / push runs a shallow clone is fine
83
+ # because the checked-out tree is what gitleaks scans.
84
+ fetch-depth: ${{ github.event_name == 'schedule' && 0 || 1 }}
85
+
86
+ - name: Install gitleaks (verified SHA-256)
87
+ run: |
88
+ set -euo pipefail
89
+ archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
90
+ url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${archive}"
91
+ curl --proto '=https' --tlsv1.2 -sSfL "$url" -o "$archive"
92
+ echo "${GITLEAKS_SHA256} ${archive}" | sha256sum --check --strict
93
+ mkdir -p "$RUNNER_TEMP/gitleaks"
94
+ tar -xzf "$archive" -C "$RUNNER_TEMP/gitleaks" gitleaks
95
+ rm -f "$archive"
96
+ echo "$RUNNER_TEMP/gitleaks" >> "$GITHUB_PATH"
97
+
98
+ - name: gitleaks version
99
+ run: gitleaks version
100
+
101
+ - name: gitleaks (working tree)
102
+ run: gitleaks dir --no-banner --redact --verbose --exit-code 1 .
103
+
104
+ - name: gitleaks (full git history)
105
+ if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
106
+ run: gitleaks git --no-banner --redact --verbose --exit-code 1 .
@@ -23,7 +23,10 @@ The `--with-ci` bundle adds these defaults:
23
23
  - Package-manager caches are disabled in CI to avoid cache-poisoning bridges.
24
24
  - Lockfile source verification rejects git dependencies and non-registry tarball URLs.
25
25
  - CodeQL, OpenSSF Scorecard, zizmor, Dependabot, and CODEOWNERS are generated.
26
+ - A weekly scheduled `opengrep.yml` runs a second SAST engine (Aikido's LGPL-2.1 fork of Semgrep) against the repository using the curated `p/security-audit`, `p/owasp-top-ten`, `p/cwe-top-25`, `p/javascript`, `p/typescript`, `p/nodejs`, and `p/secrets` rule packs. The Opengrep binary is downloaded from a pinned GitHub release and its sigstore cosign signature is verified against the official `opengrep/opengrep` release identity before it runs — no third-party action sits in the supply chain just for this scan. Running two SAST engines (CodeQL + Opengrep) catches different bug classes than either alone; see Aikido's [Ultimate SAST Guide](https://www.aikido.dev/blog/ultimate-sast-guide-static-application-security-testing) for why a layered SAST posture is the recommended default.
26
27
  - A daily scheduled `vuln-scan.yml` runs the package manager's audit against the committed lockfile so newly-disclosed CVEs are surfaced even when no PR or push has run CI (SOC 2 CC7.1 continuous-vulnerability-management evidence).
28
+ - A weekly scheduled `dast.yml` boots the built application and runs an OWASP ZAP baseline scan against it. CodeQL + Opengrep + the `verify:*` family are the SAST half of the posture; this is the DAST half — see Aikido's [SAST vs DAST](https://www.aikido.dev/blog/sast-vs-dast-what-you-need-to-now) write-up for why both matter. Findings are summarized in the workflow log and the job fails on HIGH-risk alerts; MEDIUM / LOW / INFO are surfaced for triage but non-blocking.
29
+ - A `secret-scan.yml` workflow runs [gitleaks](https://github.com/gitleaks/gitleaks) against the working tree on every PR / push and against the **full git history** on a daily schedule. The gitleaks binary is downloaded from a pinned official release and verified by SHA-256 before execution so the scan does not introduce a new third-party action into the supply chain. Findings block the merge; matched values are redacted from the public log. The rationale (and the remediation playbook for a confirmed leak) follows Aikido's [Secrets Detection guide](https://www.aikido.dev/blog/secret-detection-application-security): a secret in any commit, branch, or tag should be treated as compromised, and detection must consider the entire history — not just the latest snapshot — alongside GitHub-native push protection.
27
30
  - No npm publish workflow is generated: this scaffold is a REST API service, not a published package. If you later carve out a reusable library you can opt into npm trusted publishing yourself.
28
31
 
29
32
  ## Container hardening
@@ -56,11 +59,107 @@ SHA-pinned open-source tooling:
56
59
  - All findings are uploaded as SARIF to the GitHub **Code Scanning**
57
60
  tab alongside CodeQL.
58
61
 
62
+ ## Runtime hardening (`docker run`, Compose, Fly machines)
63
+
64
+ The image build is only half the story. The Aikido
65
+ ["Container Security — The Dev Guide"](https://www.aikido.dev/blog/container-security-guide)
66
+ write-up calls out that most container compromises chain a known
67
+ software CVE with a permissive runtime configuration: a container
68
+ launched with the default capability set, no resource limits, and a
69
+ writable root filesystem turns a single RCE into a host compromise.
70
+ Apply these flags wherever you launch the image outside of Kubernetes
71
+ (the **Pod security on Kubernetes** entry below covers the equivalent
72
+ `PodSpec` settings):
73
+
74
+ - `--read-only` — root filesystem is read-only. The shipped image
75
+ needs no writable paths at runtime; pair with
76
+ `--tmpfs /tmp:rw,noexec,nosuid,size=64m` if your app writes
77
+ temp files.
78
+ - `--cap-drop=ALL` — drop every Linux capability. The Node.js
79
+ runtime under `tini` needs none of the default set
80
+ (`CHOWN`, `DAC_OVERRIDE`, `NET_BIND_SERVICE`, …); rebind the
81
+ listening port to `>= 1024` (the shipped `EXPOSE 3000`
82
+ already does) so you do not need `NET_BIND_SERVICE` back.
83
+ - `--security-opt=no-new-privileges:true` — block `setuid` /
84
+ `setgid` / file-capability escalation inside the container, so a
85
+ compromised handler cannot regain privileges the image dropped.
86
+ - `--security-opt=seccomp=default` — keep Docker's default seccomp
87
+ profile (it is on by default, but some orchestrators disable it;
88
+ re-assert it explicitly).
89
+ - `--pids-limit=256` — cap process count. Prevents fork-bomb style
90
+ DoS from a compromised handler.
91
+ - `--memory=512m --memory-swap=512m --cpus=1.0` — enforce resource
92
+ limits. The framework's `loadShedding()` middleware refuses
93
+ requests under event-loop / RSS / heap pressure, but the
94
+ kernel-side limits are the backstop that makes a runaway
95
+ handler a `137 OOMKilled` instead of a noisy neighbour.
96
+ - **Never** pass `--privileged`, `-v /var/run/docker.sock:…`, or
97
+ `--pid=host` / `--network=host` to this image. None of them are
98
+ needed by a stateless HTTP service and each one converts a
99
+ container escape into a host takeover.
100
+
101
+ Equivalent `compose.yml` snippet (drop into the service block your
102
+ deploy uses):
103
+
104
+ ```yaml
105
+ services:
106
+ app:
107
+ image: ghcr.io/your-org/your-app:sha-<digest>
108
+ read_only: true
109
+ tmpfs:
110
+ - /tmp:rw,noexec,nosuid,size=64m
111
+ cap_drop: ["ALL"]
112
+ security_opt:
113
+ - "no-new-privileges:true"
114
+ pids_limit: 256
115
+ mem_limit: 512m
116
+ memswap_limit: 512m
117
+ cpus: 1.0
118
+ restart: unless-stopped
119
+ ```
120
+
121
+ Fly.io, Render, Railway, and similar PaaS surfaces apply most of these
122
+ defaults for you, but check the platform documentation — `--privileged`
123
+ and host network are still escape hatches you should keep off.
124
+
125
+ ## Cloud posture (operator checklist)
126
+
127
+ The framework cannot author your cloud configuration for you, but the
128
+ [Aikido "Top 7 Cloud Security Vulnerabilities"](https://www.aikido.dev/blog/top-cloud-security-vulnerabilities)
129
+ write-up maps cleanly onto a short checklist. Adopt these alongside the
130
+ container and CI defaults above:
131
+
132
+ - **IMDSv2 only.** On EC2 / equivalent, require token-based IMDSv2 and disable
133
+ IMDSv1 at the launch-template / instance-metadata-options level. Combined
134
+ with the framework's `fetchGuard()` on user-controlled outbound fetches,
135
+ this closes the Capital One 2019 / Pandoc CVE-2025-51591 SSRF → IAM chain.
136
+ - **Least-privilege execution role.** Scope the workload's IAM role to the
137
+ minimum actions and resource ARNs the handler actually calls (e.g.
138
+ `s3:GetObject` on a single bucket prefix, never `s3:*`). Enable IAM Access
139
+ Analyzer / unused-permission scans and prune quarterly.
140
+ - **Pod security on Kubernetes.** Deploy with `runAsNonRoot: true`,
141
+ `readOnlyRootFilesystem: true`, `allowPrivilegeEscalation: false`,
142
+ `capabilities: { drop: ["ALL"] }`, and `automountServiceAccountToken: false`
143
+ on pods that do not call the kube API. The shipped `Dockerfile` already
144
+ runs as non-root UID 1001 with no writable paths required at runtime, so
145
+ these settings work out of the box.
146
+ - **Network segmentation.** Keep development, staging, and production in
147
+ separate cloud accounts / projects. Apply default-deny `NetworkPolicy` (k8s)
148
+ or Security Group egress rules so a compromised workload cannot freely
149
+ reach the metadata service, the database tier, or the internet.
150
+ - **Log agent isolation.** If you run FluentBit / Vector / Fluentd as a
151
+ sidecar or DaemonSet, run it under its own ServiceAccount with read-only
152
+ access to the log path, pin the agent image to a digest, and subscribe to
153
+ its CVE feed (FluentBit had CVE-2025-12969 auth bypass + CVE-2025-12972
154
+ path traversal disclosed in 2025). The framework's structured logger
155
+ already redacts known credential shapes **before** the line leaves the
156
+ process, so a compromised agent receives pre-redacted data.
157
+
59
158
  ## Required repository settings
60
159
 
61
160
  Before relying on these files for a company project:
62
161
 
63
162
  1. Replace `@your-org/security-team` in `.github/CODEOWNERS` or pass `--code-owner` when scaffolding.
64
- 2. Protect the `main` branch and require the CI, CodeQL, Scorecard, and zizmor checks.
163
+ 2. Protect the `main` branch and require the CI, CodeQL, Opengrep, Scorecard, and zizmor checks.
65
164
  3. Enable GitHub secret scanning and push protection.
66
165
  4. Keep `ignore-scripts=true` and the `pnpm-workspace.yaml` supply-chain settings on when using pnpm.
@@ -7,6 +7,9 @@
7
7
  /.github/ __CODE_OWNER__
8
8
  /.github/workflows/ __CODE_OWNER__
9
9
  /.github/workflows/vuln-scan.yml __CODE_OWNER__
10
+ /.github/workflows/osv-scan.yml __CODE_OWNER__
11
+ /.github/workflows/dast.yml __CODE_OWNER__
12
+ /.github/workflows/secret-scan.yml __CODE_OWNER__
10
13
  /.github/dependabot.yml __CODE_OWNER__
11
14
  /.github/CODEOWNERS __CODE_OWNER__
12
15
 
@@ -1,18 +1,28 @@
1
- # Container security scan generated by create-daloy --with-ci.
1
+ # Container + IaC security scan generated by create-daloy --with-ci.
2
2
  #
3
3
  # Inspired by the controls in
4
- # https://snyk.io/blog/scale-container-security-effortlessly/ — but built
5
- # only from SHA-pinned, free, open-source GitHub Actions:
4
+ # https://snyk.io/blog/scale-container-security-effortlessly/ and
5
+ # Aikido's "IaC security scanning for Terraform & Kubernetes
6
+ # misconfigurations"
7
+ # (https://www.aikido.dev/blog/iac-security-scanning-terraform-kubernetes-misconfigurations)
8
+ # — but built only from SHA-pinned, free, open-source GitHub Actions:
6
9
  #
7
10
  # - hadolint lints the Dockerfile for known anti-patterns and unsafe
8
11
  # instructions (CIS Docker Benchmark coverage).
9
- # - Trivy scans the source tree (config + secrets + vulnerable lockfile
10
- # entries), then scans the built image for OS + language CVEs.
12
+ # - Trivy filesystem scan with the `misconfig` scanner enabled catches
13
+ # IaC misconfigurations in Terraform (`*.tf` / `*.tfvars`),
14
+ # Kubernetes manifests (`k8s/**`, `deployment.yaml`,
15
+ # `kustomization*.yaml`), Helm charts, Dockerfiles, and
16
+ # CloudFormation templates — plus secrets and vulnerable lockfile
17
+ # entries. This runs even when no Dockerfile is present, so a
18
+ # project that only ships Terraform / K8s still gets the Aikido-style
19
+ # IaC posture review on every PR.
20
+ # - Trivy image scan covers OS + language CVEs once the image is built.
11
21
  # - SARIF results are uploaded to the Code Scanning tab so findings show
12
22
  # up in the same place as CodeQL.
13
23
  #
14
- # The job is a no-op if the project has no Dockerfile at the repo root, so
15
- # it is safe to keep enabled for templates that disable the container.
24
+ # The Dockerfile-specific steps (hadolint, build, image scan) are a no-op
25
+ # if the project has no Dockerfile at the repo root.
16
26
 
17
27
  name: Container scan
18
28
 
@@ -26,6 +36,17 @@ on:
26
36
  - "package-lock.json"
27
37
  - "yarn.lock"
28
38
  - "bun.lock"
39
+ - "**/*.tf"
40
+ - "**/*.tfvars"
41
+ - "**/*.hcl"
42
+ - "k8s/**"
43
+ - "helm/**"
44
+ - "**/deployment.yaml"
45
+ - "**/deployment.yml"
46
+ - "**/kustomization.yaml"
47
+ - "**/kustomization.yml"
48
+ - "**/Chart.yaml"
49
+ - "**/values.yaml"
29
50
  - ".github/workflows/container-scan.yml"
30
51
  push:
31
52
  branches: [main]
@@ -37,6 +58,17 @@ on:
37
58
  - "package-lock.json"
38
59
  - "yarn.lock"
39
60
  - "bun.lock"
61
+ - "**/*.tf"
62
+ - "**/*.tfvars"
63
+ - "**/*.hcl"
64
+ - "k8s/**"
65
+ - "helm/**"
66
+ - "**/deployment.yaml"
67
+ - "**/deployment.yml"
68
+ - "**/kustomization.yaml"
69
+ - "**/kustomization.yml"
70
+ - "**/Chart.yaml"
71
+ - "**/values.yaml"
40
72
  - ".github/workflows/container-scan.yml"
41
73
  schedule:
42
74
  # Weekly run catches newly-disclosed base-image CVEs even when the
@@ -130,12 +162,17 @@ jobs:
130
162
  sarif_file: hadolint.sarif
131
163
  category: hadolint
132
164
 
133
- - name: Trivy filesystem scan (config + secrets + vulns)
134
- if: steps.detect.outputs.present == 'true'
165
+ - name: Trivy filesystem scan (IaC misconfig + secrets + vulns)
166
+ # `misconfig` is the IaC posture scanner — Terraform, Kubernetes,
167
+ # Helm, Dockerfile, CloudFormation — per Aikido's "IaC security
168
+ # scanning for Terraform & Kubernetes misconfigurations" article.
169
+ # It runs unconditionally so a project that only ships Terraform
170
+ # or K8s (no Dockerfile) still gets reviewed on every PR.
135
171
  uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
136
172
  with:
137
173
  scan-type: fs
138
174
  scan-ref: .
175
+ scanners: vuln,secret,misconfig
139
176
  severity: HIGH,CRITICAL
140
177
  ignore-unfixed: true
141
178
  format: sarif
@@ -143,7 +180,6 @@ jobs:
143
180
  exit-code: "0"
144
181
 
145
182
  - name: Upload Trivy filesystem SARIF
146
- if: steps.detect.outputs.present == 'true'
147
183
  uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
148
184
  with:
149
185
  sarif_file: trivy-fs.sarif
@@ -0,0 +1,177 @@
1
+ # Dynamic Application Security Testing (DAST) generated by
2
+ # create-daloy --with-ci.
3
+ #
4
+ # Why this exists:
5
+ # The shipped `ci.yml` + `codeql.yml` + `scorecard.yml` + `zizmor.yml`
6
+ # stack is the SAST half of your security posture — static analysis
7
+ # of source, dependencies, and workflows. Aikido's "SAST vs DAST:
8
+ # what you need to know"
9
+ # (https://www.aikido.dev/blog/sast-vs-dast-what-you-need-to-now)
10
+ # argues that SAST and DAST catch different bug classes and that you
11
+ # want both. This workflow adds the dynamic half: it boots your built
12
+ # application, points OWASP ZAP's baseline scanner at it, and fails
13
+ # the run on any HIGH-risk finding.
14
+ #
15
+ # Because Daloy is JSON-first and the framework already enforces
16
+ # header sanitization, body limits, router path-traversal rejection,
17
+ # and `secureHeaders()` defaults, the baseline scan usually goes
18
+ # green out of the box on a fresh scaffold. The value is the
19
+ # *continuous* probe — newly-disclosed ZAP rules apply to your
20
+ # unchanged code, and runtime regressions (a misconfigured CORS
21
+ # policy, a header you set with CRLF in it, a redirect to an
22
+ # open-redirect target) get caught before they hit production.
23
+ #
24
+ # Trigger surface:
25
+ # * Weekly cron — continuous coverage against newly-disclosed rules.
26
+ # * `workflow_dispatch` — on-demand probe for security review.
27
+ # Deliberately NOT on every PR: a baseline scan takes several
28
+ # minutes and the same surface is exercised by your test suite on
29
+ # every PR via `ci.yml`.
30
+ #
31
+ # Customizing:
32
+ # * The "Start application" step expects `pnpm build` to produce
33
+ # `dist/index.js` and a `PORT` env var to control the listen port,
34
+ # matching the shipped `node-basic` template. If your app starts
35
+ # differently, edit the `nohup` line below.
36
+ # * If your app needs seed data, secrets, or a database, add them
37
+ # before the start step (use `vars` / `secrets` in the GitHub
38
+ # Environment).
39
+ # * To probe a deployed staging URL instead of a local build, drop
40
+ # the "Set up …", "Install", "Build", "Start application", and
41
+ # "Wait for target" steps and point the ZAP `docker run` line at
42
+ # `https://staging.example.com`.
43
+
44
+ name: DAST
45
+
46
+ on:
47
+ schedule:
48
+ # 07:42 UTC weekly (Wed) — offset from the other scheduled scans
49
+ # so they do not all queue at once.
50
+ - cron: "42 7 * * 3"
51
+ workflow_dispatch:
52
+
53
+ permissions: {}
54
+
55
+ concurrency:
56
+ group: dast-${{ github.workflow }}-${{ github.ref }}
57
+ cancel-in-progress: true
58
+
59
+ jobs:
60
+ zap-baseline:
61
+ name: ZAP baseline scan
62
+ runs-on: ubuntu-latest
63
+ timeout-minutes: 25
64
+ permissions:
65
+ contents: read
66
+
67
+ steps:
68
+ - name: Harden runner
69
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
70
+ with:
71
+ egress-policy: audit
72
+ disable-sudo: true
73
+ disable-file-monitoring: false
74
+
75
+ - name: Checkout
76
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
77
+ with:
78
+ persist-credentials: false
79
+ show-progress: false
80
+
81
+ - name: Set up pnpm
82
+ uses: pnpm/action-setup@ac6db6d3c1f721f886538a378a2d73e85697340a # v6
83
+ with:
84
+ version: 11.1.3
85
+ run_install: false
86
+
87
+ - name: Set up Node.js
88
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
89
+ with:
90
+ node-version: 24
91
+
92
+ - name: Install dependencies (no scripts)
93
+ run: pnpm install --frozen-lockfile --ignore-scripts
94
+ env:
95
+ npm_config_ignore_scripts: "true"
96
+
97
+ - name: Build
98
+ run: pnpm build
99
+
100
+ - name: Start application (background)
101
+ run: |
102
+ mkdir -p dast-results
103
+ chmod 777 dast-results
104
+ nohup node dist/index.js > dast-results/server.log 2>&1 &
105
+ echo $! > dast-results/server.pid
106
+ env:
107
+ PORT: "3000"
108
+ NODE_ENV: production
109
+
110
+ - name: Wait for target to be reachable
111
+ run: |
112
+ for i in $(seq 1 30); do
113
+ if curl -fsS --max-time 2 http://localhost:3000/healthz > /dev/null \
114
+ || curl -fsS --max-time 2 http://localhost:3000/ > /dev/null; then
115
+ echo "Target ready after ${i} attempt(s)."
116
+ exit 0
117
+ fi
118
+ sleep 1
119
+ done
120
+ echo "::error::DAST target did not become reachable in 30s."
121
+ cat dast-results/server.log || true
122
+ exit 1
123
+
124
+ - name: Run OWASP ZAP baseline scan
125
+ run: |
126
+ docker pull ghcr.io/zaproxy/zaproxy:stable
127
+ docker run --rm --network host \
128
+ -v "$PWD/dast-results:/zap/wrk/:rw" \
129
+ -t ghcr.io/zaproxy/zaproxy:stable \
130
+ zap-baseline.py \
131
+ -t http://localhost:3000 \
132
+ -J zap-report.json \
133
+ -w zap-report.md \
134
+ -r zap-report.html \
135
+ -I \
136
+ || true
137
+
138
+ - name: Stop application
139
+ if: always()
140
+ run: |
141
+ if [ -f dast-results/server.pid ]; then
142
+ kill "$(cat dast-results/server.pid)" || true
143
+ fi
144
+
145
+ - name: Summarize ZAP findings and fail on HIGH
146
+ if: always()
147
+ run: |
148
+ report=dast-results/zap-report.json
149
+ if [ ! -f "$report" ]; then
150
+ echo "::warning::No ZAP JSON report produced (scan may have failed to start)."
151
+ exit 0
152
+ fi
153
+ counts=$(node -e "
154
+ const r = JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8'));
155
+ const c = { high: 0, medium: 0, low: 0, info: 0 };
156
+ for (const site of r.site ?? []) {
157
+ for (const alert of site.alerts ?? []) {
158
+ const code = String(alert.riskcode);
159
+ if (code === '3') c.high++;
160
+ else if (code === '2') c.medium++;
161
+ else if (code === '1') c.low++;
162
+ else c.info++;
163
+ }
164
+ }
165
+ process.stdout.write(JSON.stringify(c));
166
+ " "$report")
167
+ echo "ZAP findings: ${counts}"
168
+ high=$(node -e "process.stdout.write(String(JSON.parse(process.argv[1]).high))" "$counts")
169
+ if [ -f dast-results/zap-report.md ]; then
170
+ echo "----- ZAP markdown report -----"
171
+ cat dast-results/zap-report.md
172
+ echo "----- end report -----"
173
+ fi
174
+ if [ "${high}" -gt 0 ]; then
175
+ echo "::error::ZAP reported ${high} HIGH-risk finding(s) against your application surface."
176
+ exit 1
177
+ fi
@@ -0,0 +1,94 @@
1
+ # Starter deployment workflow generated by create-daloy --with-ci.
2
+ #
3
+ # This workflow is intentionally manual-only so a fresh scaffold does not fail
4
+ # on every push before you wire in secrets, variables, and environment
5
+ # approvals. Once you are ready, add your preferred push/tag trigger.
6
+ #
7
+ __DEPLOY_HEADER__
8
+
9
+ name: Deploy
10
+
11
+ on:
12
+ workflow_dispatch:
13
+
14
+ permissions: {}
15
+
16
+ concurrency:
17
+ group: deploy-${{ github.workflow }}-${{ github.ref }}
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ verify:
22
+ name: Verify before deploy
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 20
25
+ permissions:
26
+ contents: read
27
+
28
+ steps:
29
+ - name: Harden runner
30
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
31
+ with:
32
+ egress-policy: audit
33
+ disable-sudo: true
34
+
35
+ - name: Checkout
36
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
37
+ with:
38
+ persist-credentials: false
39
+ show-progress: false
40
+
41
+ __SETUP_PACKAGE_MANAGER_STEP__
42
+
43
+ - name: Set up Node.js
44
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
45
+ with:
46
+ node-version: 24
47
+ # Package-manager caching is intentionally disabled. Shared caches
48
+ # can bridge fork PRs into trusted branches when mis-keyed.
49
+
50
+ __SETUP_BUN_RUNTIME_STEP__
51
+
52
+ - name: Install dependencies
53
+ run: __INSTALL_COMMAND__
54
+ env:
55
+ npm_config_ignore_scripts: "true"
56
+
57
+ __DEPLOY_VERIFY_LOCKFILE_STEP__
58
+
59
+ - name: Typecheck
60
+ run: __TYPECHECK_COMMAND__
61
+
62
+ - name: Test
63
+ run: __TEST_COMMAND__
64
+
65
+ __BUILD_STEP__
66
+
67
+ deploy:
68
+ name: __DEPLOY_JOB_NAME__
69
+ needs: verify
70
+ __DEPLOY_REF_GUARD__
71
+ runs-on: ubuntu-latest
72
+ timeout-minutes: 20
73
+ environment:
74
+ name: production
75
+ permissions:
76
+ contents: read
77
+ __DEPLOY_JOB_PERMISSIONS__
78
+
79
+ steps:
80
+ - name: Harden runner
81
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
82
+ with:
83
+ egress-policy: audit
84
+ disable-sudo: true
85
+
86
+ - name: Checkout
87
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
88
+ with:
89
+ persist-credentials: false
90
+ show-progress: false
91
+
92
+ __DEPLOY_SETUP_STEPS__
93
+
94
+ __DEPLOY_STEPS__