create-daloy 0.24.0 → 0.34.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 (35) hide show
  1. package/README.md +18 -5
  2. package/bin/create-daloy.mjs +130 -37
  3. package/package.json +5 -3
  4. package/sbom.cdx.json +56 -0
  5. package/sbom.spdx.json +42 -0
  6. package/templates/_ci/deno/SECURITY.md +5 -1
  7. package/templates/_ci/deno/_github/dependabot.yml +12 -1
  8. package/templates/_ci/deno/_github/workflows/container-scan.yml +158 -0
  9. package/templates/_ci/node/SECURITY.md +33 -3
  10. package/templates/_ci/node/_github/CODEOWNERS +1 -1
  11. package/templates/_ci/node/_github/dependabot.yml +12 -1
  12. package/templates/_ci/node/_github/workflows/container-scan.yml +177 -0
  13. package/templates/_ci/node/_github/workflows/vuln-scan.yml +89 -0
  14. package/templates/bun-basic/AGENTS.md +10 -0
  15. package/templates/bun-basic/README.md +10 -0
  16. package/templates/bun-basic/_Dockerfile +57 -0
  17. package/templates/bun-basic/_dockerignore +11 -0
  18. package/templates/bun-basic/package.json +1 -1
  19. package/templates/cloudflare-worker/_Dockerfile +56 -0
  20. package/templates/cloudflare-worker/_dockerignore +13 -0
  21. package/templates/cloudflare-worker/package.json +1 -1
  22. package/templates/cloudflare-worker/src/index.ts +17 -0
  23. package/templates/deno-basic/_Dockerfile +66 -0
  24. package/templates/deno-basic/_dockerignore +10 -0
  25. package/templates/deno-basic/deno.json +2 -2
  26. package/templates/node-basic/AGENTS.md +10 -0
  27. package/templates/node-basic/README.md +10 -0
  28. package/templates/node-basic/_Dockerfile +23 -10
  29. package/templates/node-basic/package.json +1 -1
  30. package/templates/vercel-edge/AGENTS.md +10 -0
  31. package/templates/vercel-edge/README.md +10 -0
  32. package/templates/vercel-edge/_Dockerfile +55 -0
  33. package/templates/vercel-edge/_dockerignore +13 -0
  34. package/templates/vercel-edge/package.json +1 -1
  35. package/templates/_ci/node/_github/workflows/release.yml +0 -125
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
+ <p align="center">
2
+ <a href="https://daloyjs.dev">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://daloyjs.dev/assets/banner-x-1500x500.png">
5
+ <img alt="DaloyJS — Contract-first REST APIs for Node · Bun · Deno · Workers · Edge" src="https://daloyjs.dev/assets/banner-light-1280x426.png" width="100%">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
1
10
  # create-daloy
2
11
 
3
12
  Scaffold a new [DaloyJS](https://github.com/daloyjs/daloy) project in seconds.
@@ -144,17 +153,21 @@ For Node-style templates, the bundle adds:
144
153
  - `.github/workflows/ci.yml` with top-level `permissions: {}`, pinned actions,
145
154
  `harden-runner`, `persist-credentials: false`, no package-manager cache, and
146
155
  install scripts disabled.
147
- - `.github/workflows/release.yml` as a disabled-by-default npm trusted publishing
148
- skeleton. It only publishes when `NPM_PUBLISH_ENABLED=true`, the package is no
149
- longer private, and the protected `npm-publish` environment is configured.
156
+ - `.github/workflows/vuln-scan.yml` a daily scheduled SCA cron that runs the
157
+ package manager's audit against the committed lockfile. Catches CVEs disclosed
158
+ *after* the last PR or push and provides SOC 2 CC7.1
159
+ ([continuous vulnerability management](https://www.aikido.dev/blog/a-guide-to-automating-technical-vulnerability-management-for-soc-2))
160
+ evidence even when developers are not touching the repo.
150
161
  - CodeQL, OpenSSF Scorecard, zizmor, Dependabot, CODEOWNERS, and `SECURITY.md`.
151
162
  - `scripts/verify-lockfile-sources.mjs` plus a `verify:lockfile` package script
152
163
  that rejects git dependencies and non-registry tarball URLs in text lockfiles.
153
164
 
165
+ The bundle deliberately does **not** generate an npm publish workflow.
166
+ `create-daloy` scaffolds REST API services, not libraries; if you later carve
167
+ out a reusable package, opt into npm trusted publishing yourself.
168
+
154
169
  For `deno-basic`, `--with-ci` generates a Deno-native CI workflow plus CodeQL,
155
170
  Scorecard, zizmor, Dependabot for GitHub Actions, CODEOWNERS, and `SECURITY.md`.
156
- It does not generate an npm release workflow because the Deno template has no
157
- `package.json`.
158
171
 
159
172
  If you omit `--code-owner`, the generated CODEOWNERS file uses
160
173
  `@your-org/security-team` as a placeholder. Replace it before relying on branch
@@ -250,15 +250,27 @@ function renderBox(lines, options = {}) {
250
250
  return out.join("\n");
251
251
  }
252
252
 
253
- // Block-letter "DALOYJS" banner rendered with a left-to-right golden
254
- // gradient (dark goldenrod bright gold) on truecolor terminals. Falls back
255
- // to a single bold-yellow line on 256-color TTYs and to plain text in dumb
256
- // terminals. The shape is built from half-block characters so it stays
257
- // compact (2 lines tall) and each glyph is 3 columns wide with a single
258
- // space between letters, keeping the top and bottom rows perfectly aligned.
259
- const LOGO_LINES = [
260
- " \u2588\u2580\u2584 \u2584\u2580\u2588 \u2588 \u2588\u2580\u2588 \u2588 \u2588 \u2588 \u2584\u2580\u2580 ",
261
- " \u2588\u2584\u2580 \u2588\u2580\u2588 \u2588\u2584\u2584 \u2588\u2584\u2588 \u2588 \u2584\u2584\u2588 \u2584\u2584\u2580 ",
253
+ // "Flowing waves" banner that mirrors the DaloyJS brand mark: three sine
254
+ // curves cascading left-to-right in sky-blue tones. Each row is rendered
255
+ // with a left-to-right gradient on truecolor terminals, falls back to ANSI
256
+ // cyan on 256-color TTYs, and degrades to ASCII tildes in dumb terminals.
257
+ //
258
+ // "Daloy" means "flow" in Filipino the three waves represent contracts,
259
+ // requests, and responses moving cleanly between client and server.
260
+ const LOGO_WAVE_LINES = SUPPORTS_UNICODE
261
+ ? [
262
+ "\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F",
263
+ "\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F",
264
+ "\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F\u223F",
265
+ ]
266
+ : ["~".repeat(25), "~".repeat(25), "~".repeat(25)];
267
+
268
+ // Sky-blue palette tuned to match `tailwindcss` sky-200/400/700 — the same
269
+ // colors used in the wordmark on https://daloyjs.dev.
270
+ const LOGO_WAVE_GRADIENTS = [
271
+ { start: [186, 230, 253], end: [125, 211, 252] }, // sky-200 -> sky-300
272
+ { start: [56, 189, 248], end: [14, 165, 233] }, // sky-400 -> sky-500
273
+ { start: [2, 132, 199], end: [3, 105, 161] }, // sky-600 -> sky-700
262
274
  ];
263
275
 
264
276
  function gradientLine(line, startRgb, endRgb) {
@@ -278,25 +290,26 @@ function gradientLine(line, startRgb, endRgb) {
278
290
 
279
291
  function printBanner(version) {
280
292
  if (!SUPPORTS_UNICODE) {
281
- console.log(`\n${color(COLORS.bold + COLORS.yellow, "create-daloy")} ${color(COLORS.dim, `v${version}`)}`);
293
+ console.log(`\n${color(COLORS.bold + COLORS.cyan, "create-daloy")} ${color(COLORS.dim, `v${version}`)}`);
282
294
  console.log(color(COLORS.dim, "Contract-first REST APIs for Node, Bun, Deno, Vercel Edge, and Workers"));
283
295
  console.log(color(COLORS.dim, "https://daloyjs.dev\n"));
284
296
  return;
285
297
  }
286
- // Golden gradient: DarkGoldenrod Gold. Evokes the DaloyJS "flow of gold"
287
- // brand and stays legible on both light and dark terminal backgrounds.
288
- const start = [184, 134, 11]; // DarkGoldenrod
289
- const end = [255, 215, 0]; // Gold
290
- console.log("");
291
- for (const line of LOGO_LINES) {
292
- console.log(` ${gradientLine(line, start, end)}`);
298
+ for (let i = 0; i < LOGO_WAVE_LINES.length; i += 1) {
299
+ const { start, end } = LOGO_WAVE_GRADIENTS[i];
300
+ console.log(` ${gradientLine(LOGO_WAVE_LINES[i], start, end)}`);
293
301
  }
302
+ // Centered wordmark beneath the waves: "Daloy" in neutral text, "JS" in
303
+ // brand sky-blue. Mirrors the SVG lockup used on the website.
304
+ const wordmark = `${color(COLORS.bold + COLORS.white, "Daloy")}${color(COLORS.bold + COLORS.cyan, "JS")}`;
305
+ const wordmarkPadding = " ".repeat(Math.max(0, Math.floor((stringWidth(LOGO_WAVE_LINES[0]) - 7) / 2)));
306
+ console.log(` ${wordmarkPadding}${wordmark}`);
294
307
  // Build the welcome content lines (each contains its own ANSI color codes).
295
- const headline = `${color(COLORS.bold + COLORS.yellow, "Welcome to DaloyJS")} ${color(COLORS.gray, `\u2014 v${version}`)}`;
308
+ const headline = `${color(COLORS.bold + COLORS.cyan, "Welcome to DaloyJS")} ${color(COLORS.gray, `\u2014 v${version}`)}`;
296
309
  const subline = color(COLORS.dim, "Contract-first REST APIs for Node, Bun, Deno, Vercel Edge, and Workers.");
297
310
  const docs = `${color(COLORS.gray, "docs:")} ${color(COLORS.cyan, "https://daloyjs.dev/docs")}`;
298
311
  console.log("");
299
- console.log(renderBox([headline, subline, "", docs], { accent: COLORS.yellow }));
312
+ console.log(renderBox([headline, subline, "", docs], { accent: COLORS.cyan }));
300
313
  console.log("");
301
314
  }
302
315
 
@@ -556,6 +569,73 @@ async function normalizePackageManagerFiles(dir, packageManager) {
556
569
  }
557
570
  }
558
571
 
572
+ function dockerInstallSnippet(packageManager) {
573
+ if (packageManager === "npm") {
574
+ return {
575
+ copy: "COPY package.json package-lock.json* npm-shrinkwrap.json* ./",
576
+ run: "RUN npm ci --ignore-scripts",
577
+ text: "npm ci --ignore-scripts",
578
+ };
579
+ }
580
+ if (packageManager === "yarn") {
581
+ return {
582
+ copy: "COPY package.json yarn.lock* ./",
583
+ run: "RUN corepack enable && yarn install --frozen-lockfile --ignore-scripts",
584
+ text: "yarn install --frozen-lockfile --ignore-scripts",
585
+ };
586
+ }
587
+ if (packageManager === "bun") {
588
+ return {
589
+ copy: "COPY package.json bun.lock* bun.lockb* ./",
590
+ run: "RUN bun install --frozen-lockfile --ignore-scripts",
591
+ text: "bun install --frozen-lockfile --ignore-scripts",
592
+ };
593
+ }
594
+ return {
595
+ copy: "COPY package.json pnpm-lock.yaml* ./",
596
+ run: "RUN corepack enable && corepack prepare pnpm@latest --activate && \\\n pnpm install --frozen-lockfile --ignore-scripts",
597
+ text: "pnpm install --frozen-lockfile --ignore-scripts",
598
+ };
599
+ }
600
+
601
+ async function patchDockerfileForPackageManager(dir, packageManager) {
602
+ const file = path.join(dir, "Dockerfile");
603
+ if (!existsSync(file)) return;
604
+
605
+ const raw = await readFile(file, "utf8");
606
+ const install = dockerInstallSnippet(packageManager);
607
+ const defaultPnpmInstallBlock = [
608
+ "COPY package.json pnpm-lock.yaml* ./",
609
+ "RUN corepack enable && corepack prepare pnpm@latest --activate && \\",
610
+ " pnpm install --frozen-lockfile --ignore-scripts",
611
+ ].join("\n");
612
+ let next = raw.replace(
613
+ "COPY package.json pnpm-lock.yaml* ./\nRUN corepack enable && corepack prepare pnpm@latest --activate && \\\n pnpm install --frozen-lockfile --ignore-scripts",
614
+ `${install.copy}\n${install.run}`,
615
+ );
616
+ next = next.replace(defaultPnpmInstallBlock, `${install.copy}\n${install.run}`);
617
+
618
+ if (packageManager !== "pnpm") {
619
+ next = next.replaceAll(
620
+ "pnpm install --frozen-lockfile --ignore-scripts",
621
+ install.text,
622
+ );
623
+ next = next.replaceAll("pnpm build", runScriptCommand(packageManager, "build"));
624
+ }
625
+
626
+ if (packageManager === "bun" && next.includes("FROM ${NODE_IMAGE} AS builder")) {
627
+ if (!next.includes("ARG BUN_IMAGE=")) {
628
+ next = next.replace(
629
+ "ARG NODE_IMAGE=node:24-alpine\n",
630
+ "ARG NODE_IMAGE=node:24-alpine\nARG BUN_IMAGE=oven/bun:1-alpine\n",
631
+ );
632
+ }
633
+ next = next.replace("FROM ${NODE_IMAGE} AS builder", "FROM ${BUN_IMAGE} AS builder");
634
+ }
635
+
636
+ if (next !== raw) await writeFile(file, next, "utf8");
637
+ }
638
+
559
639
  function hasPackageScript(packageJson, scriptName) {
560
640
  return typeof packageJson?.scripts?.[scriptName] === "string";
561
641
  }
@@ -584,6 +664,21 @@ function auditCommand(packageManager) {
584
664
  return "";
585
665
  }
586
666
 
667
+ function auditStepName(packageManager, suffix = "") {
668
+ const baseName = packageManager === "bun" ? "Audit dependencies" : "Audit production dependencies";
669
+ return suffix ? `${baseName} ${suffix}` : baseName;
670
+ }
671
+
672
+ // Extra full-tree audit (includes devDependencies) for package managers that
673
+ // expose separate prod/full scopes. Surfaced for triage but non-blocking so a
674
+ // low-severity dev-tool advisory does not page the on-call.
675
+ function auditFullCommand(packageManager) {
676
+ if (packageManager === "pnpm") return "pnpm audit";
677
+ if (packageManager === "npm") return "npm audit";
678
+ if (packageManager === "yarn") return "yarn audit";
679
+ return "";
680
+ }
681
+
587
682
  function setupPackageManagerStep(packageManager) {
588
683
  if (packageManager === "pnpm") {
589
684
  return ` - name: Set up pnpm
@@ -612,15 +707,6 @@ function workflowStep(name, command) {
612
707
  run: ${command}`;
613
708
  }
614
709
 
615
- function multilineWorkflowStep(name, command) {
616
- return ` - name: ${name}
617
- run: |
618
- ${command
619
- .split("\n")
620
- .map((line) => ` ${line}`)
621
- .join("\n")}`;
622
- }
623
-
624
710
  async function readPackageJsonIfPresent(dir) {
625
711
  const file = path.join(dir, "package.json");
626
712
  if (!existsSync(file)) return null;
@@ -643,15 +729,18 @@ function renderCiReplacements({ packageManager, template, packageJson, codeOwner
643
729
  const setupPm = setupPackageManagerStep(packageManager);
644
730
  const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
645
731
  const audit = auditCommand(packageManager);
732
+ const auditFull = auditFullCommand(packageManager);
646
733
  const buildStep = hasPackageScript(packageJson, "build") ? workflowStep("Build", runScriptCommand(packageManager, "build")) : "";
647
- const auditStep = audit ? workflowStep("Audit production dependencies", audit) : "";
648
- const tagVersionCheck = `set -eu
649
- tag_version="\${GITHUB_REF_NAME#v}"
650
- pkg_version="$(node -p "require('./package.json').version")"
651
- if [ "$tag_version" != "$pkg_version" ]; then
652
- echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $pkg_version"
653
- exit 1
654
- fi`;
734
+ const auditStep = audit ? workflowStep(auditStepName(packageManager), audit) : "";
735
+ // vuln-scan.yml: production-tree audit is blocking, full-tree audit is
736
+ // advisory (continue-on-error) so a low-severity dev-tool advisory does
737
+ // not page the on-call on a daily cron.
738
+ const auditProdStep = audit
739
+ ? workflowStep(auditStepName(packageManager, "(blocking)"), audit)
740
+ : workflowStep("No package-manager audit available", "echo 'No audit command available for this package manager; consider switching to npm/pnpm/yarn/bun.'");
741
+ const auditFullStep = auditFull
742
+ ? ` - name: Audit full dependency tree (advisory)\n run: ${auditFull}\n continue-on-error: true`
743
+ : "";
655
744
 
656
745
  return new Map([
657
746
  ["__CODE_OWNER__", codeOwner],
@@ -663,7 +752,8 @@ fi`;
663
752
  ["__TEST_COMMAND__", runScriptCommand(packageManager, "test")],
664
753
  ["__BUILD_STEP__", buildStep],
665
754
  ["__AUDIT_STEP__", auditStep],
666
- ["__TAG_VERSION_CHECK_STEP__", multilineWorkflowStep("Verify tag matches package.json version", tagVersionCheck)],
755
+ ["__AUDIT_PROD_STEP__", auditProdStep],
756
+ ["__AUDIT_FULL_STEP__", auditFullStep],
667
757
  ]);
668
758
  }
669
759
 
@@ -1305,6 +1395,9 @@ async function main() {
1305
1395
  await patchPackageJson(targetDir, projectName, packageManager);
1306
1396
  logStep("Package metadata written", projectName);
1307
1397
  await patchTemplateTextFiles(targetDir, packageManager);
1398
+ if (packageManager !== "pnpm") {
1399
+ await patchDockerfileForPackageManager(targetDir, packageManager);
1400
+ }
1308
1401
  await normalizePackageManagerFiles(targetDir, packageManager);
1309
1402
  if (packageManager !== "pnpm") {
1310
1403
  logStep("Package-manager config normalized", packageManager);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "0.24.0",
3
+ "version": "0.34.0",
4
4
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,7 +35,9 @@
35
35
  "files": [
36
36
  "bin",
37
37
  "templates",
38
- "README.md"
38
+ "README.md",
39
+ "sbom.cdx.json",
40
+ "sbom.spdx.json"
39
41
  ],
40
42
  "publishConfig": {
41
43
  "access": "public"
@@ -43,4 +45,4 @@
43
45
  "scripts": {
44
46
  "test": "node --test test/**/*.test.mjs"
45
47
  }
46
- }
48
+ }
package/sbom.cdx.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "bomFormat": "CycloneDX",
3
+ "specVersion": "1.5",
4
+ "serialNumber": "urn:uuid:82c87bf3-10d1-59f3-9cc6-48dd49603d42",
5
+ "version": 1,
6
+ "metadata": {
7
+ "timestamp": "2026-05-21T23:55:46.185Z",
8
+ "tools": [
9
+ {
10
+ "vendor": "DaloyJS",
11
+ "name": "daloy-generate-sbom",
12
+ "version": "0.34.0"
13
+ }
14
+ ],
15
+ "authors": [],
16
+ "component": {
17
+ "type": "library",
18
+ "bom-ref": "pkg:npm/create-daloy@0.34.0",
19
+ "name": "create-daloy",
20
+ "version": "0.34.0",
21
+ "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
22
+ "purl": "pkg:npm/create-daloy@0.34.0",
23
+ "licenses": [
24
+ {
25
+ "license": {
26
+ "id": "MIT"
27
+ }
28
+ }
29
+ ],
30
+ "externalReferences": [
31
+ {
32
+ "type": "vcs",
33
+ "url": "https://github.com/daloyjs/daloy"
34
+ },
35
+ {
36
+ "type": "website",
37
+ "url": "https://github.com/daloyjs/daloy/tree/main/packages/create-daloy#readme"
38
+ }
39
+ ],
40
+ "swid": {
41
+ "tagId": "swidtag-create-daloy-0.34.0",
42
+ "name": "create-daloy",
43
+ "version": "0.34.0",
44
+ "tagVersion": 0,
45
+ "patch": false
46
+ }
47
+ }
48
+ },
49
+ "components": [],
50
+ "dependencies": [
51
+ {
52
+ "ref": "pkg:npm/create-daloy@0.34.0",
53
+ "dependsOn": []
54
+ }
55
+ ]
56
+ }
package/sbom.spdx.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "spdxVersion": "SPDX-2.3",
3
+ "dataLicense": "CC0-1.0",
4
+ "SPDXID": "SPDXRef-DOCUMENT",
5
+ "name": "create-daloy-0.34.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.34.0-82c87bf3-10d1-59f3-9cc6-48dd49603d42",
7
+ "creationInfo": {
8
+ "created": "2026-05-21T23:55:46.185Z",
9
+ "creators": [
10
+ "Tool: daloy-generate-sbom",
11
+ "Organization: DaloyJS"
12
+ ],
13
+ "licenseListVersion": "3.24"
14
+ },
15
+ "packages": [
16
+ {
17
+ "SPDXID": "SPDXRef-Package-create-daloy",
18
+ "name": "create-daloy",
19
+ "versionInfo": "0.34.0",
20
+ "downloadLocation": "https://github.com/daloyjs/daloy",
21
+ "filesAnalyzed": false,
22
+ "licenseConcluded": "MIT",
23
+ "licenseDeclared": "MIT",
24
+ "copyrightText": "NOASSERTION",
25
+ "supplier": "NOASSERTION",
26
+ "externalRefs": [
27
+ {
28
+ "referenceCategory": "PACKAGE-MANAGER",
29
+ "referenceType": "purl",
30
+ "referenceLocator": "pkg:npm/create-daloy@0.34.0"
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ "relationships": [
36
+ {
37
+ "spdxElementId": "SPDXRef-DOCUMENT",
38
+ "relationshipType": "DESCRIBES",
39
+ "relatedSpdxElement": "SPDXRef-Package-create-daloy"
40
+ }
41
+ ]
42
+ }
@@ -20,7 +20,11 @@ The `--with-ci` bundle adds these defaults:
20
20
  - Third-party actions are pinned to commit SHAs.
21
21
  - `actions/checkout` uses `persist-credentials: false`.
22
22
  - Deno CI runs `deno task typecheck` and `deno task test` with the template's narrow permission flags.
23
- - CodeQL, OpenSSF Scorecard, zizmor, Dependabot for GitHub Actions, and CODEOWNERS are generated.
23
+ - If the generated project keeps the `Dockerfile`, container scanning lints it
24
+ with hadolint, scans the source tree and built image with Trivy, and warns
25
+ when `FROM` lines are not pinned to an immutable `@sha256:` digest.
26
+ - CodeQL, OpenSSF Scorecard, zizmor, Dependabot for GitHub Actions and Docker
27
+ base images, and CODEOWNERS are generated.
24
28
 
25
29
  ## Required repository settings
26
30
 
@@ -12,4 +12,15 @@ updates:
12
12
  patterns:
13
13
  - "*"
14
14
  commit-message:
15
- prefix: "chore(actions)"
15
+ prefix: "chore(actions)"
16
+
17
+ # Keep the Deno runtime base image (Dockerfile `FROM`) patched. Combined
18
+ # with the `container-scan.yml` workflow, this catches base-image CVEs early.
19
+ - package-ecosystem: docker
20
+ directory: "/"
21
+ schedule:
22
+ interval: weekly
23
+ day: monday
24
+ open-pull-requests-limit: 5
25
+ commit-message:
26
+ prefix: "chore(docker)"
@@ -0,0 +1,158 @@
1
+ # Container security scan generated by create-daloy --with-ci.
2
+ #
3
+ # The Deno template has no npm package manager, but its Dockerfile still
4
+ # deserves the same container review as Node-based scaffolds:
5
+ #
6
+ # - hadolint lints the Dockerfile for known anti-patterns and unsafe
7
+ # instructions (CIS Docker Benchmark coverage).
8
+ # - Trivy scans the source tree (config + secrets + vulnerable lockfile
9
+ # entries), then scans the built image for OS + language CVEs.
10
+ # - SARIF results are uploaded to the Code Scanning tab so findings show
11
+ # up in the same place as CodeQL.
12
+
13
+ name: Container scan
14
+
15
+ on:
16
+ pull_request:
17
+ paths:
18
+ - "Dockerfile"
19
+ - ".dockerignore"
20
+ - "deno.json"
21
+ - "deno.lock"
22
+ - ".github/workflows/container-scan.yml"
23
+ push:
24
+ branches: [main]
25
+ paths:
26
+ - "Dockerfile"
27
+ - ".dockerignore"
28
+ - "deno.json"
29
+ - "deno.lock"
30
+ - ".github/workflows/container-scan.yml"
31
+ schedule:
32
+ # Weekly run catches newly-disclosed base-image CVEs even when the
33
+ # Dockerfile itself has not changed.
34
+ - cron: "17 6 * * 1"
35
+
36
+ permissions: {}
37
+
38
+ concurrency:
39
+ group: container-scan-${{ github.workflow }}-${{ github.ref }}
40
+ cancel-in-progress: true
41
+
42
+ jobs:
43
+ scan:
44
+ name: Lint + scan container
45
+ runs-on: ubuntu-latest
46
+ timeout-minutes: 20
47
+ permissions:
48
+ contents: read
49
+ security-events: write
50
+
51
+ steps:
52
+ - name: Harden runner
53
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
54
+ with:
55
+ egress-policy: audit
56
+ disable-sudo: true
57
+
58
+ - name: Checkout
59
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
60
+ with:
61
+ persist-credentials: false
62
+ show-progress: false
63
+
64
+ - name: Detect Dockerfile
65
+ id: detect
66
+ run: |
67
+ if [ -f Dockerfile ]; then
68
+ echo "present=true" >> "$GITHUB_OUTPUT"
69
+ else
70
+ echo "present=false" >> "$GITHUB_OUTPUT"
71
+ echo "::notice::No Dockerfile found at the repo root; skipping container scan."
72
+ fi
73
+
74
+ - name: Pin check (FROM @sha256 digest)
75
+ # Floating tags like `denoland/deno:alpine` silently re-resolve to
76
+ # fresh digests on every build. Pinning `FROM <image>@sha256:<digest>`
77
+ # makes the base reproducible; the Docker Dependabot ecosystem then
78
+ # opens PRs when newer digests are published.
79
+ if: steps.detect.outputs.present == 'true'
80
+ run: |
81
+ unpinned=0
82
+ while IFS= read -r line; do
83
+ ref="${line#FROM }"
84
+ ref="${ref#from }"
85
+ ref="${ref%% AS *}"
86
+ ref="${ref%% as *}"
87
+ ref="${ref## }"
88
+ ref="${ref%% }"
89
+ case "$ref" in
90
+ scratch|"\${"*|*"@sha256:"*) ;;
91
+ *)
92
+ echo "::warning file=Dockerfile::FROM '$ref' is not pinned to a @sha256:<digest>. Override at build time with --build-arg DENO_IMAGE=denoland/deno:alpine@sha256:<digest> or hard-code the digest and let Dependabot keep it fresh."
93
+ unpinned=$((unpinned + 1))
94
+ ;;
95
+ esac
96
+ done < <(grep -E '^(FROM|from) ' Dockerfile || true)
97
+ if [ "$unpinned" -gt 0 ]; then
98
+ echo "::notice::$unpinned unpinned FROM line(s)."
99
+ fi
100
+
101
+ - name: Lint Dockerfile (hadolint)
102
+ if: steps.detect.outputs.present == 'true'
103
+ uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
104
+ with:
105
+ dockerfile: Dockerfile
106
+ format: sarif
107
+ output-file: hadolint.sarif
108
+ no-fail: true
109
+
110
+ - name: Upload hadolint SARIF
111
+ if: steps.detect.outputs.present == 'true'
112
+ uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
113
+ with:
114
+ sarif_file: hadolint.sarif
115
+ category: hadolint
116
+
117
+ - name: Trivy filesystem scan (config + secrets + vulns)
118
+ if: steps.detect.outputs.present == 'true'
119
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
120
+ with:
121
+ scan-type: fs
122
+ scan-ref: .
123
+ severity: HIGH,CRITICAL
124
+ ignore-unfixed: true
125
+ format: sarif
126
+ output: trivy-fs.sarif
127
+ exit-code: "0"
128
+
129
+ - name: Upload Trivy filesystem SARIF
130
+ if: steps.detect.outputs.present == 'true'
131
+ uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
132
+ with:
133
+ sarif_file: trivy-fs.sarif
134
+ category: trivy-fs
135
+
136
+ - name: Build image (no push)
137
+ if: steps.detect.outputs.present == 'true'
138
+ run: |
139
+ docker build --pull --load -t local/app:scan .
140
+
141
+ - name: Trivy image scan
142
+ if: steps.detect.outputs.present == 'true'
143
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
144
+ with:
145
+ image-ref: local/app:scan
146
+ severity: HIGH,CRITICAL
147
+ ignore-unfixed: true
148
+ format: sarif
149
+ output: trivy-image.sarif
150
+ exit-code: "1"
151
+ vuln-type: os,library
152
+
153
+ - name: Upload Trivy image SARIF
154
+ if: always() && steps.detect.outputs.present == 'true'
155
+ uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
156
+ with:
157
+ sarif_file: trivy-image.sarif
158
+ category: trivy-image
@@ -23,7 +23,38 @@ 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
- - npm publishing is disabled until `NPM_PUBLISH_ENABLED=true` is set and npm trusted publishing is configured.
26
+ - 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).
27
+ - 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
+
29
+ ## Container hardening
30
+
31
+ If you scaffolded the `node-basic` template (the only one that ships a
32
+ `Dockerfile`), the following defaults are also enabled. They cover the same
33
+ ground the Snyk container-security guidance recommends, using only free,
34
+ SHA-pinned open-source tooling:
35
+
36
+ - Runtime stage is `node:24-alpine` with `tini` as PID 1 and **no `curl`** —
37
+ the `HEALTHCHECK` uses BusyBox `wget` already shipped in the image.
38
+ - Base image is consumed through `ARG NODE_IMAGE`, so production builds can
39
+ pin to an immutable digest: `docker build --build-arg NODE_IMAGE=node:24-alpine@sha256:<digest> .`.
40
+ - App runs as a non-root user (uid 1001) with `STOPSIGNAL SIGTERM`; the
41
+ framework's graceful-shutdown drain fires on container stop.
42
+ - `Dockerfile` is monitored by Dependabot's `docker` ecosystem so the base
43
+ image gets bumped automatically when CVEs land.
44
+ - The `.github/workflows/container-scan.yml` workflow runs on every PR
45
+ and weekly on `main`:
46
+ - **Pin check** emits a PR annotation when any `FROM` line is not
47
+ pinned to a `@sha256:<digest>` (Aikido x Root.io 2026,
48
+ [Harden your containers without the headaches](https://www.aikido.dev/blog/aikido-x-root-io-harden-your-containers-without-the-headaches)).
49
+ Non-blocking so a fresh scaffold goes green; pair with the `docker`
50
+ Dependabot ecosystem to keep the pinned digest current.
51
+ - **hadolint** lints the `Dockerfile` (CIS Docker Benchmark coverage).
52
+ - **Trivy** scans the source tree for secrets, config issues, and
53
+ vulnerable lockfile entries.
54
+ - **Trivy** builds the image and scans it for OS + language CVEs
55
+ (`HIGH`/`CRITICAL`, `--ignore-unfixed`); merges block on CRITICAL.
56
+ - All findings are uploaded as SARIF to the GitHub **Code Scanning**
57
+ tab alongside CodeQL.
27
58
 
28
59
  ## Required repository settings
29
60
 
@@ -32,5 +63,4 @@ Before relying on these files for a company project:
32
63
  1. Replace `@your-org/security-team` in `.github/CODEOWNERS` or pass `--code-owner` when scaffolding.
33
64
  2. Protect the `main` branch and require the CI, CodeQL, Scorecard, and zizmor checks.
34
65
  3. Enable GitHub secret scanning and push protection.
35
- 4. Configure a protected `npm-publish` Environment before enabling npm publish.
36
- 5. Keep `ignore-scripts=true` and the `pnpm-workspace.yaml` supply-chain settings on when using pnpm.
66
+ 4. Keep `ignore-scripts=true` and the `pnpm-workspace.yaml` supply-chain settings on when using pnpm.
@@ -6,7 +6,7 @@
6
6
  # Workflow / CI / CD.
7
7
  /.github/ __CODE_OWNER__
8
8
  /.github/workflows/ __CODE_OWNER__
9
- /.github/workflows/release.yml __CODE_OWNER__
9
+ /.github/workflows/vuln-scan.yml __CODE_OWNER__
10
10
  /.github/dependabot.yml __CODE_OWNER__
11
11
  /.github/CODEOWNERS __CODE_OWNER__
12
12
 
@@ -25,4 +25,15 @@ updates:
25
25
  dev-dependencies:
26
26
  dependency-type: development
27
27
  commit-message:
28
- prefix: "chore(deps)"
28
+ prefix: "chore(deps)"
29
+
30
+ # Keep the runtime base image (Dockerfile `FROM`) patched. Combined with
31
+ # the `container-scan.yml` workflow, this catches base-image CVEs early.
32
+ - package-ecosystem: docker
33
+ directory: "/"
34
+ schedule:
35
+ interval: weekly
36
+ day: monday
37
+ open-pull-requests-limit: 5
38
+ commit-message:
39
+ prefix: "chore(docker)"