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.
- package/README.md +62 -6
- package/bin/create-daloy.mjs +344 -25
- package/package.json +2 -2
- package/sbom.cdx.json +13 -9
- package/sbom.spdx.json +5 -5
- package/templates/_ci/deno/SECURITY.md +31 -2
- package/templates/_ci/deno/_github/CODEOWNERS +2 -0
- package/templates/_ci/deno/_github/workflows/container-scan.yml +42 -8
- package/templates/_ci/deno/_github/workflows/deploy.yml +183 -0
- package/templates/_ci/deno/_github/workflows/opengrep.yml +137 -0
- package/templates/_ci/deno/_github/workflows/osv-scan.yml +121 -0
- package/templates/_ci/deno/_github/workflows/secret-scan.yml +106 -0
- package/templates/_ci/node/SECURITY.md +100 -1
- package/templates/_ci/node/_github/CODEOWNERS +3 -0
- package/templates/_ci/node/_github/workflows/container-scan.yml +46 -10
- package/templates/_ci/node/_github/workflows/dast.yml +177 -0
- package/templates/_ci/node/_github/workflows/deploy.yml +94 -0
- package/templates/_ci/node/_github/workflows/opengrep.yml +169 -0
- package/templates/_ci/node/_github/workflows/osv-scan.yml +135 -0
- package/templates/_ci/node/_github/workflows/secret-scan.yml +106 -0
- package/templates/bun-basic/AGENTS.md +12 -0
- package/templates/bun-basic/_gitignore +5 -0
- package/templates/bun-basic/package.json +3 -2
- package/templates/cloudflare-worker/AGENTS.md +13 -0
- package/templates/cloudflare-worker/_gitignore +9 -0
- package/templates/cloudflare-worker/_npmrc +2 -1
- package/templates/cloudflare-worker/package.json +3 -2
- package/templates/deno-basic/AGENTS.md +12 -0
- package/templates/deno-basic/_gitignore +5 -0
- package/templates/deno-basic/deno.json +2 -2
- package/templates/node-basic/AGENTS.md +12 -0
- package/templates/node-basic/_gitignore +5 -0
- package/templates/node-basic/_npmrc +2 -2
- package/templates/node-basic/package.json +1 -1
- package/templates/vercel-edge/AGENTS.md +12 -0
- package/templates/vercel-edge/_gitignore +5 -0
- package/templates/vercel-edge/_npmrc +2 -1
- 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/
|
|
5
|
-
#
|
|
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
|
|
10
|
-
#
|
|
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
|
|
15
|
-
#
|
|
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 (
|
|
134
|
-
|
|
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__
|