create-daloy 0.1.20 → 0.1.22
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 +42 -1
- package/bin/create-daloy.mjs +202 -5
- package/package.json +1 -1
- package/templates/_ci/deno/SECURITY.md +32 -0
- package/templates/_ci/deno/_github/CODEOWNERS +17 -0
- package/templates/_ci/deno/_github/dependabot.yml +15 -0
- package/templates/_ci/deno/_github/workflows/ci.yml +47 -0
- package/templates/_ci/deno/_github/workflows/codeql.yml +56 -0
- package/templates/_ci/deno/_github/workflows/scorecard.yml +46 -0
- package/templates/_ci/deno/_github/workflows/zizmor.yml +38 -0
- package/templates/_ci/node/SECURITY.md +36 -0
- package/templates/_ci/node/_github/CODEOWNERS +25 -0
- package/templates/_ci/node/_github/dependabot.yml +28 -0
- package/templates/_ci/node/_github/workflows/ci.yml +65 -0
- package/templates/_ci/node/_github/workflows/codeql.yml +56 -0
- package/templates/_ci/node/_github/workflows/release.yml +125 -0
- package/templates/_ci/node/_github/workflows/scorecard.yml +46 -0
- package/templates/_ci/node/_github/workflows/zizmor.yml +38 -0
- package/templates/_ci/node/scripts/verify-lockfile-sources.mjs +61 -0
- package/templates/bun-basic/pnpm-workspace.yaml +25 -0
- package/templates/cloudflare-worker/pnpm-workspace.yaml +24 -0
- package/templates/node-basic/pnpm-workspace.yaml +24 -0
- package/templates/vercel-edge/pnpm-workspace.yaml +24 -0
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ The CLI is interactive when arguments are missing. It will ask you for:
|
|
|
18
18
|
`deno-basic` runtime template
|
|
19
19
|
- Whether to install dependencies
|
|
20
20
|
- Whether to initialize a git repository
|
|
21
|
+
- Whether to add hardened GitHub Actions and security/governance files
|
|
21
22
|
|
|
22
23
|
Interactive runs use a polished terminal UI with a DaloyJS welcome banner,
|
|
23
24
|
arrow-key template and package-manager pickers, progress indicators, and a
|
|
@@ -30,6 +31,8 @@ script-friendly transcript with the same decisions and next steps.
|
|
|
30
31
|
pnpm create daloy@latest my-api \
|
|
31
32
|
--template node-basic \
|
|
32
33
|
--package-manager pnpm \
|
|
34
|
+
--with-ci \
|
|
35
|
+
--code-owner @acme/security \
|
|
33
36
|
--install \
|
|
34
37
|
--git
|
|
35
38
|
```
|
|
@@ -44,6 +47,8 @@ pnpm create daloy@latest my-api \
|
|
|
44
47
|
| `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to interactive. |
|
|
45
48
|
| `--git` / `--no-git` | Initialize a git repository. Defaults to interactive. |
|
|
46
49
|
| `--minimal` | Strip the bookstore demo route and the built-in `/docs` + `/openapi.json` routes so only the framework bootstrap and `/healthz` ship. |
|
|
50
|
+
| `--with-ci` / `--no-ci` | Add the hardened GitHub Actions, Dependabot, CODEOWNERS, SECURITY.md, and lockfile-source verification bundle. |
|
|
51
|
+
| `--code-owner <owner>` | Replace the CODEOWNERS placeholder when `--with-ci` is used, for example `@acme/security`. |
|
|
47
52
|
| `--force` | Overwrite an existing non-empty directory. |
|
|
48
53
|
| `--yes` | Accept all defaults; never prompt. |
|
|
49
54
|
| `--help` | Print usage and exit. |
|
|
@@ -118,14 +123,50 @@ Sentinel comments (`// daloy-minimal:strip-start <tag>` /
|
|
|
118
123
|
`// daloy-minimal:strip-end <tag>`) survive a default scaffold so you can
|
|
119
124
|
re-run with `--minimal`, or delete the marked blocks by hand later.
|
|
120
125
|
|
|
126
|
+
## Hardened GitHub security bundle
|
|
127
|
+
|
|
128
|
+
Pass `--with-ci` when you want the generated project to start with the same
|
|
129
|
+
security posture as a serious company repo:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pnpm create daloy@latest my-api \
|
|
133
|
+
--template node-basic \
|
|
134
|
+
--package-manager pnpm \
|
|
135
|
+
--with-ci \
|
|
136
|
+
--code-owner @acme/security
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For Node-style templates, the bundle adds:
|
|
140
|
+
|
|
141
|
+
- `.github/workflows/ci.yml` with top-level `permissions: {}`, pinned actions,
|
|
142
|
+
`harden-runner`, `persist-credentials: false`, no package-manager cache, and
|
|
143
|
+
install scripts disabled.
|
|
144
|
+
- `.github/workflows/release.yml` as a disabled-by-default npm trusted publishing
|
|
145
|
+
skeleton. It only publishes when `NPM_PUBLISH_ENABLED=true`, the package is no
|
|
146
|
+
longer private, and the protected `npm-publish` environment is configured.
|
|
147
|
+
- CodeQL, OpenSSF Scorecard, zizmor, Dependabot, CODEOWNERS, and `SECURITY.md`.
|
|
148
|
+
- `scripts/verify-lockfile-sources.mjs` plus a `verify:lockfile` package script
|
|
149
|
+
that rejects git dependencies and non-registry tarball URLs in text lockfiles.
|
|
150
|
+
|
|
151
|
+
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow plus CodeQL,
|
|
152
|
+
Scorecard, zizmor, Dependabot for GitHub Actions, CODEOWNERS, and `SECURITY.md`.
|
|
153
|
+
It does not generate an npm release workflow because the Deno template has no
|
|
154
|
+
`package.json`.
|
|
155
|
+
|
|
156
|
+
If you omit `--code-owner`, the generated CODEOWNERS file uses
|
|
157
|
+
`@your-org/security-team` as a placeholder. Replace it before relying on branch
|
|
158
|
+
protection. You should also enable GitHub secret scanning, push protection, and
|
|
159
|
+
required status checks in the repository settings.
|
|
160
|
+
|
|
121
161
|
## What the CLI guarantees
|
|
122
162
|
|
|
123
163
|
- Zero runtime dependencies (uses only Node built-ins) for a clean supply-chain footprint.
|
|
124
164
|
- A modern terminal experience with Unicode/color capability detection and ASCII fallbacks.
|
|
125
165
|
- Templates are copied verbatim from this package's `templates/` directory.
|
|
126
|
-
- Files and folders prefixed with `_` are renamed on copy (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_agents/` → `.agents/`) to survive npm packing.
|
|
166
|
+
- Files and folders prefixed with `_` are renamed on copy (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_github/` → `.github`, `_agents/` → `.agents/`) to survive npm packing.
|
|
127
167
|
- pnpm-specific `.npmrc` hardening is kept only when you choose `pnpm`; other package managers get a clean project without unsupported config warnings.
|
|
128
168
|
- pnpm projects ship with `ignore-scripts=true`, `minimum-release-age=1440`, `verify-store-integrity=true`, `prefer-frozen-lockfile=true`, and `strict-peer-dependencies=true` by default.
|
|
169
|
+
- `--with-ci` projects ship with pinned GitHub Actions workflows, CODEOWNERS, Dependabot, SECURITY.md, and lockfile-source verification.
|
|
129
170
|
- The CLI never executes template scripts and never makes network calls beyond the package manager you select.
|
|
130
171
|
|
|
131
172
|
## AI agent helper files
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
15
15
|
const TEMPLATES_DIR = path.join(PKG_ROOT, "templates");
|
|
16
|
+
const CI_TEMPLATES_DIR = path.join(TEMPLATES_DIR, "_ci");
|
|
16
17
|
|
|
17
18
|
const TEMPLATE_OPTIONS = [
|
|
18
19
|
{
|
|
@@ -56,6 +57,7 @@ const RENAME_ON_COPY = new Map([
|
|
|
56
57
|
["_gitignore", ".gitignore"],
|
|
57
58
|
["_npmrc", ".npmrc"],
|
|
58
59
|
["_env.example", ".env.example"],
|
|
60
|
+
["_github", ".github"],
|
|
59
61
|
// Directory: holds skill files for AI coding agents under
|
|
60
62
|
// `.agents/skills/<skill-name>/SKILL.md`. Templates author this as
|
|
61
63
|
// `_agents/` so npm pack does not drop the dotfolder during publish.
|
|
@@ -72,6 +74,8 @@ const NO_PACKAGE_JSON_TEMPLATES = new Set(["deno-basic"]);
|
|
|
72
74
|
// `daloy-minimal:strip-start <tag>` / `daloy-minimal:strip-end <tag>`
|
|
73
75
|
// sentinels.
|
|
74
76
|
const MINIMAL_STRIP_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".md"]);
|
|
77
|
+
const CI_PLACEHOLDER_EXTENSIONS = new Set([".json", ".md", ".mjs", ".yaml", ".yml"]);
|
|
78
|
+
const CI_PLACEHOLDER_FILES = new Set(["CODEOWNERS"]);
|
|
75
79
|
|
|
76
80
|
// ----------------------------------------------------------------------------
|
|
77
81
|
// Terminal capability detection + style primitives.
|
|
@@ -308,6 +312,8 @@ ${heading("Options")}
|
|
|
308
312
|
${color(COLORS.green, "--install / --no-install")} Install dependencies after scaffolding.
|
|
309
313
|
${color(COLORS.green, "--git / --no-git")} Initialize a git repository.
|
|
310
314
|
${color(COLORS.green, "--minimal")} Strip the bookstore + Swagger/OpenAPI demo routes.
|
|
315
|
+
${color(COLORS.green, "--with-ci / --no-ci")} Add hardened GitHub Actions + governance files.
|
|
316
|
+
${color(COLORS.green, "--code-owner <owner>")} CODEOWNERS owner for --with-ci, e.g. @acme/security.
|
|
311
317
|
${color(COLORS.green, "--force")} Overwrite an existing non-empty directory.
|
|
312
318
|
${color(COLORS.green, "--yes, -y")} Accept all defaults; never prompt.
|
|
313
319
|
${color(COLORS.green, "--help, -h")} Print this help.
|
|
@@ -353,6 +359,8 @@ function parseArgs(argv) {
|
|
|
353
359
|
version: false,
|
|
354
360
|
listTemplates: false,
|
|
355
361
|
minimal: false,
|
|
362
|
+
ci: undefined,
|
|
363
|
+
codeOwner: undefined,
|
|
356
364
|
};
|
|
357
365
|
const args = [...argv];
|
|
358
366
|
while (args.length) {
|
|
@@ -363,6 +371,10 @@ function parseArgs(argv) {
|
|
|
363
371
|
else if (a === "--yes" || a === "-y") out.yes = true;
|
|
364
372
|
else if (a === "--force") out.force = true;
|
|
365
373
|
else if (a === "--minimal") out.minimal = true;
|
|
374
|
+
else if (a === "--with-ci") out.ci = true;
|
|
375
|
+
else if (a === "--no-ci") out.ci = false;
|
|
376
|
+
else if (a === "--code-owner") out.codeOwner = args.shift();
|
|
377
|
+
else if (a?.startsWith("--code-owner=")) out.codeOwner = a.slice("--code-owner=".length);
|
|
366
378
|
else if (a === "--install") out.install = true;
|
|
367
379
|
else if (a === "--no-install") out.install = false;
|
|
368
380
|
else if (a === "--git") out.git = true;
|
|
@@ -391,6 +403,11 @@ function detectPackageManager() {
|
|
|
391
403
|
}
|
|
392
404
|
|
|
393
405
|
const VALID_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
406
|
+
// Match the GitHub CODEOWNERS owner grammar: a personal handle (@user), an
|
|
407
|
+
// organization team (@org/team), or an email address. Anything else is
|
|
408
|
+
// rejected so the scaffolded CODEOWNERS stays meaningful for branch protection.
|
|
409
|
+
const VALID_CODE_OWNER =
|
|
410
|
+
/^(?:@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38})(?:\/[a-zA-Z0-9][a-zA-Z0-9._-]*)?|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})$/;
|
|
394
411
|
|
|
395
412
|
function validateProjectName(name) {
|
|
396
413
|
if (!name || !name.trim()) return "Project name cannot be empty.";
|
|
@@ -521,9 +538,176 @@ function rewriteScriptsForPackageManager(scripts, pm) {
|
|
|
521
538
|
|
|
522
539
|
async function normalizePackageManagerFiles(dir, packageManager) {
|
|
523
540
|
if (packageManager === "pnpm") return;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
541
|
+
// The hardened `.npmrc` and `pnpm-workspace.yaml` only make sense for pnpm.
|
|
542
|
+
// Removing them keeps npm/yarn/bun scaffolds from inheriting pnpm-specific
|
|
543
|
+
// settings the chosen package manager would either ignore or misinterpret.
|
|
544
|
+
for (const file of [".npmrc", "pnpm-workspace.yaml"]) {
|
|
545
|
+
const target = path.join(dir, file);
|
|
546
|
+
if (existsSync(target)) {
|
|
547
|
+
await rm(target, { force: true });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function hasPackageScript(packageJson, scriptName) {
|
|
553
|
+
return typeof packageJson?.scripts?.[scriptName] === "string";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function runScriptCommand(packageManager, scriptName) {
|
|
557
|
+
if (packageManager === "pnpm") return `pnpm ${scriptName}`;
|
|
558
|
+
if (packageManager === "npm") return scriptName === "test" ? "npm test" : `npm run ${scriptName}`;
|
|
559
|
+
if (packageManager === "yarn") return scriptName === "test" ? "yarn test" : `yarn run ${scriptName}`;
|
|
560
|
+
if (packageManager === "bun") return scriptName === "test" ? "bun test" : `bun run ${scriptName}`;
|
|
561
|
+
return `${packageManager} run ${scriptName}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function installCommand(packageManager) {
|
|
565
|
+
if (packageManager === "pnpm") return "pnpm install --frozen-lockfile --ignore-scripts";
|
|
566
|
+
if (packageManager === "npm") return "npm ci --ignore-scripts";
|
|
567
|
+
if (packageManager === "yarn") return "yarn install --frozen-lockfile --ignore-scripts";
|
|
568
|
+
if (packageManager === "bun") return "bun install --frozen-lockfile --ignore-scripts";
|
|
569
|
+
return `${packageManager} install`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function auditCommand(packageManager) {
|
|
573
|
+
if (packageManager === "pnpm") return "pnpm audit --prod";
|
|
574
|
+
if (packageManager === "npm") return "npm audit --omit=dev";
|
|
575
|
+
if (packageManager === "yarn") return "yarn audit --groups dependencies";
|
|
576
|
+
if (packageManager === "bun") return "bun audit";
|
|
577
|
+
return "";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function setupPackageManagerStep(packageManager) {
|
|
581
|
+
if (packageManager === "pnpm") {
|
|
582
|
+
return ` - name: Set up pnpm
|
|
583
|
+
uses: pnpm/action-setup@ac6db6d3c1f721f886538a378a2d73e85697340a # v6
|
|
584
|
+
with:
|
|
585
|
+
version: 11.1.2
|
|
586
|
+
run_install: false`;
|
|
587
|
+
}
|
|
588
|
+
if (packageManager === "yarn") {
|
|
589
|
+
return ` - name: Enable Corepack
|
|
590
|
+
run: corepack enable`;
|
|
591
|
+
}
|
|
592
|
+
if (packageManager === "bun") return setupBunStep();
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function setupBunStep() {
|
|
597
|
+
return ` - name: Set up Bun
|
|
598
|
+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
599
|
+
with:
|
|
600
|
+
bun-version: latest`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function workflowStep(name, command) {
|
|
604
|
+
return ` - name: ${name}
|
|
605
|
+
run: ${command}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function multilineWorkflowStep(name, command) {
|
|
609
|
+
return ` - name: ${name}
|
|
610
|
+
run: |
|
|
611
|
+
${command
|
|
612
|
+
.split("\n")
|
|
613
|
+
.map((line) => ` ${line}`)
|
|
614
|
+
.join("\n")}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function readPackageJsonIfPresent(dir) {
|
|
618
|
+
const file = path.join(dir, "package.json");
|
|
619
|
+
if (!existsSync(file)) return null;
|
|
620
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function writePackageJson(dir, packageJson) {
|
|
624
|
+
await writeFile(path.join(dir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n", "utf8");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function addLockfileVerifyScript(dir) {
|
|
628
|
+
const packageJson = await readPackageJsonIfPresent(dir);
|
|
629
|
+
if (!packageJson) return;
|
|
630
|
+
packageJson.scripts ??= {};
|
|
631
|
+
packageJson.scripts["verify:lockfile"] = "node scripts/verify-lockfile-sources.mjs";
|
|
632
|
+
await writePackageJson(dir, packageJson);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function renderCiReplacements({ packageManager, template, packageJson, codeOwner }) {
|
|
636
|
+
const setupPm = setupPackageManagerStep(packageManager);
|
|
637
|
+
const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
|
|
638
|
+
const audit = auditCommand(packageManager);
|
|
639
|
+
const buildStep = hasPackageScript(packageJson, "build") ? workflowStep("Build", runScriptCommand(packageManager, "build")) : "";
|
|
640
|
+
const auditStep = audit ? workflowStep("Audit production dependencies", audit) : "";
|
|
641
|
+
const tagVersionCheck = `set -eu
|
|
642
|
+
tag_version="\${GITHUB_REF_NAME#v}"
|
|
643
|
+
pkg_version="$(node -p "require('./package.json').version")"
|
|
644
|
+
if [ "$tag_version" != "$pkg_version" ]; then
|
|
645
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $pkg_version"
|
|
646
|
+
exit 1
|
|
647
|
+
fi`;
|
|
648
|
+
|
|
649
|
+
return new Map([
|
|
650
|
+
["__CODE_OWNER__", codeOwner],
|
|
651
|
+
["__SETUP_PACKAGE_MANAGER_STEP__", setupPm],
|
|
652
|
+
["__SETUP_BUN_RUNTIME_STEP__", needsBunRuntime ? setupBunStep() : ""],
|
|
653
|
+
["__INSTALL_COMMAND__", installCommand(packageManager)],
|
|
654
|
+
["__VERIFY_LOCKFILE_COMMAND__", runScriptCommand(packageManager, "verify:lockfile")],
|
|
655
|
+
["__TYPECHECK_COMMAND__", runScriptCommand(packageManager, "typecheck")],
|
|
656
|
+
["__TEST_COMMAND__", runScriptCommand(packageManager, "test")],
|
|
657
|
+
["__BUILD_STEP__", buildStep],
|
|
658
|
+
["__AUDIT_STEP__", auditStep],
|
|
659
|
+
["__TAG_VERSION_CHECK_STEP__", multilineWorkflowStep("Verify tag matches package.json version", tagVersionCheck)],
|
|
660
|
+
]);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function replacePlaceholdersInTree(dir, replacements) {
|
|
664
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
665
|
+
for (const entry of entries) {
|
|
666
|
+
const full = path.join(dir, entry.name);
|
|
667
|
+
if (entry.isDirectory()) {
|
|
668
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
669
|
+
await replacePlaceholdersInTree(full, replacements);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (!entry.isFile()) continue;
|
|
673
|
+
if (!CI_PLACEHOLDER_EXTENSIONS.has(path.extname(entry.name)) && !CI_PLACEHOLDER_FILES.has(entry.name)) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const raw = await readFile(full, "utf8");
|
|
677
|
+
let next = raw;
|
|
678
|
+
for (const [placeholder, value] of replacements) {
|
|
679
|
+
next = next.replaceAll(placeholder, value);
|
|
680
|
+
}
|
|
681
|
+
if (next !== raw) await writeFile(full, next, "utf8");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function copyCiBundle(targetDir, template, packageManager, skipPackageManager, codeOwner) {
|
|
686
|
+
const flavor = skipPackageManager ? "deno" : "node";
|
|
687
|
+
const sourceDir = path.join(CI_TEMPLATES_DIR, flavor);
|
|
688
|
+
if (!existsSync(sourceDir)) {
|
|
689
|
+
throw new Error(`CI template bundle "${flavor}" is missing from this CLI build.`);
|
|
690
|
+
}
|
|
691
|
+
await copyTemplate(sourceDir, targetDir);
|
|
692
|
+
|
|
693
|
+
const candidate = codeOwner?.trim() ?? "";
|
|
694
|
+
if (candidate && !VALID_CODE_OWNER.test(candidate)) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const owner = candidate || "@your-org/security-team";
|
|
700
|
+
if (skipPackageManager) {
|
|
701
|
+
await replacePlaceholdersInTree(targetDir, new Map([["__CODE_OWNER__", owner]]));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
await addLockfileVerifyScript(targetDir);
|
|
706
|
+
const packageJson = await readPackageJsonIfPresent(targetDir);
|
|
707
|
+
await replacePlaceholdersInTree(
|
|
708
|
+
targetDir,
|
|
709
|
+
renderCiReplacements({ packageManager, template, packageJson, codeOwner: owner }),
|
|
710
|
+
);
|
|
527
711
|
}
|
|
528
712
|
|
|
529
713
|
/**
|
|
@@ -858,7 +1042,7 @@ function createSpinner(initialMessage) {
|
|
|
858
1042
|
};
|
|
859
1043
|
}
|
|
860
1044
|
|
|
861
|
-
function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager }) {
|
|
1045
|
+
function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi }) {
|
|
862
1046
|
const templateMeta = TEMPLATE_OPTIONS.find((option) => option.value === template);
|
|
863
1047
|
const templateLabel = templateMeta ? `${templateMeta.title} ${color(COLORS.dim, `(${template})`)}` : template;
|
|
864
1048
|
const summaryLines = [
|
|
@@ -872,6 +1056,9 @@ function printSummary({ projectName, template, packageManager, installDeps, skip
|
|
|
872
1056
|
} else {
|
|
873
1057
|
summaryLines.push(`${color(COLORS.gray, "Manager ")} ${color(COLORS.cyan, packageManager)}`);
|
|
874
1058
|
}
|
|
1059
|
+
if (withCi) {
|
|
1060
|
+
summaryLines.push(`${color(COLORS.gray, "Security ")} ${color(COLORS.cyan, "GitHub CI bundle")}`);
|
|
1061
|
+
}
|
|
875
1062
|
console.log("");
|
|
876
1063
|
console.log(renderBox(summaryLines, { accent: COLORS.green }));
|
|
877
1064
|
console.log("");
|
|
@@ -1010,6 +1197,11 @@ async function main() {
|
|
|
1010
1197
|
initGit = rl ? await askYesNo(rl, "Initialize a git repository?", true) : false;
|
|
1011
1198
|
}
|
|
1012
1199
|
|
|
1200
|
+
let withCi = opts.ci;
|
|
1201
|
+
if (withCi === undefined) {
|
|
1202
|
+
withCi = rl ? await askYesNo(rl, "Add hardened GitHub Actions and security files?", false) : false;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1013
1205
|
rl?.close();
|
|
1014
1206
|
|
|
1015
1207
|
if (interactive) {
|
|
@@ -1036,6 +1228,11 @@ async function main() {
|
|
|
1036
1228
|
}
|
|
1037
1229
|
}
|
|
1038
1230
|
|
|
1231
|
+
if (withCi) {
|
|
1232
|
+
await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner);
|
|
1233
|
+
logStep("GitHub security bundle added", skipPackageManager ? "deno" : packageManager);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1039
1236
|
if (initGit) {
|
|
1040
1237
|
const code = await run("git", ["init", "--quiet"], targetDir);
|
|
1041
1238
|
if (code === 0) {
|
|
@@ -1062,7 +1259,7 @@ async function main() {
|
|
|
1062
1259
|
}
|
|
1063
1260
|
}
|
|
1064
1261
|
|
|
1065
|
-
printSummary({ projectName, template, packageManager, installDeps, skipPackageManager });
|
|
1262
|
+
printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi });
|
|
1066
1263
|
} catch (err) {
|
|
1067
1264
|
rl?.close();
|
|
1068
1265
|
if (err && err.message === "Cancelled") {
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
This project is generated by `create-daloy`. Apply security fixes to the
|
|
6
|
+
currently deployed branch and keep dependencies current through review.
|
|
7
|
+
|
|
8
|
+
## Reporting a vulnerability
|
|
9
|
+
|
|
10
|
+
Do not open a public issue for suspected vulnerabilities. Use your company's
|
|
11
|
+
private intake process, GitHub private vulnerability reporting, or the security
|
|
12
|
+
contact for this repository.
|
|
13
|
+
|
|
14
|
+
## Generated controls
|
|
15
|
+
|
|
16
|
+
The `--with-ci` bundle adds these defaults:
|
|
17
|
+
|
|
18
|
+
- GitHub Actions workflows use top-level `permissions: {}` and job-scoped permissions.
|
|
19
|
+
- Fork pull requests use `pull_request`, not `pull_request_target`.
|
|
20
|
+
- Third-party actions are pinned to commit SHAs.
|
|
21
|
+
- `actions/checkout` uses `persist-credentials: false`.
|
|
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.
|
|
24
|
+
|
|
25
|
+
## Required repository settings
|
|
26
|
+
|
|
27
|
+
Before relying on these files for a company project:
|
|
28
|
+
|
|
29
|
+
1. Replace `@your-org/security-team` in `.github/CODEOWNERS` or pass `--code-owner` when scaffolding.
|
|
30
|
+
2. Protect the `main` branch and require the CI, CodeQL, Scorecard, and zizmor checks.
|
|
31
|
+
3. Enable GitHub secret scanning and push protection.
|
|
32
|
+
4. Keep Deno permissions narrow; do not switch tasks to `--allow-all`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Replace the placeholder owner with the GitHub user or team that must review
|
|
2
|
+
# privileged files before relying on CODEOWNERS protection.
|
|
3
|
+
|
|
4
|
+
* __CODE_OWNER__
|
|
5
|
+
|
|
6
|
+
# Workflow / CI / CD.
|
|
7
|
+
/.github/ __CODE_OWNER__
|
|
8
|
+
/.github/workflows/ __CODE_OWNER__
|
|
9
|
+
/.github/dependabot.yml __CODE_OWNER__
|
|
10
|
+
/.github/CODEOWNERS __CODE_OWNER__
|
|
11
|
+
|
|
12
|
+
# Files that affect dependency resolution or runtime permissions.
|
|
13
|
+
/deno.json __CODE_OWNER__
|
|
14
|
+
/deno.lock __CODE_OWNER__
|
|
15
|
+
|
|
16
|
+
# Vulnerability handling.
|
|
17
|
+
/SECURITY.md __CODE_OWNER__
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Hardened Deno CI generated by create-daloy --with-ci.
|
|
2
|
+
name: CI
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
verify:
|
|
18
|
+
name: Verify
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 20
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Harden runner
|
|
26
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
27
|
+
with:
|
|
28
|
+
egress-policy: audit
|
|
29
|
+
disable-sudo: true
|
|
30
|
+
disable-file-monitoring: false
|
|
31
|
+
|
|
32
|
+
- name: Checkout
|
|
33
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
34
|
+
with:
|
|
35
|
+
persist-credentials: false
|
|
36
|
+
show-progress: false
|
|
37
|
+
|
|
38
|
+
- name: Set up Deno
|
|
39
|
+
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
|
40
|
+
with:
|
|
41
|
+
deno-version: v2.x
|
|
42
|
+
|
|
43
|
+
- name: Typecheck
|
|
44
|
+
run: deno task typecheck
|
|
45
|
+
|
|
46
|
+
- name: Test
|
|
47
|
+
run: deno task test
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "37 4 * * 1"
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
analyze:
|
|
15
|
+
name: Analyze (${{ matrix.language }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 30
|
|
18
|
+
permissions:
|
|
19
|
+
security-events: write
|
|
20
|
+
packages: read
|
|
21
|
+
actions: read
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
include:
|
|
28
|
+
- language: javascript-typescript
|
|
29
|
+
build-mode: none
|
|
30
|
+
- language: actions
|
|
31
|
+
build-mode: none
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- name: Harden runner
|
|
35
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
36
|
+
with:
|
|
37
|
+
egress-policy: audit
|
|
38
|
+
disable-sudo: true
|
|
39
|
+
|
|
40
|
+
- name: Checkout
|
|
41
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
42
|
+
with:
|
|
43
|
+
persist-credentials: false
|
|
44
|
+
show-progress: false
|
|
45
|
+
|
|
46
|
+
- name: Initialize CodeQL
|
|
47
|
+
uses: github/codeql-action/init@52485aec7be33610227643b0fe83936b8b5f061a # v3
|
|
48
|
+
with:
|
|
49
|
+
languages: ${{ matrix.language }}
|
|
50
|
+
build-mode: ${{ matrix.build-mode }}
|
|
51
|
+
queries: security-extended,security-and-quality
|
|
52
|
+
|
|
53
|
+
- name: Perform CodeQL Analysis
|
|
54
|
+
uses: github/codeql-action/analyze@52485aec7be33610227643b0fe83936b8b5f061a # v3
|
|
55
|
+
with:
|
|
56
|
+
category: "/language:${{ matrix.language }}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Scorecard
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
branch_protection_rule:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: "23 5 * * 2"
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
analysis:
|
|
14
|
+
name: Scorecard analysis
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 15
|
|
17
|
+
permissions:
|
|
18
|
+
security-events: write
|
|
19
|
+
id-token: write
|
|
20
|
+
contents: read
|
|
21
|
+
actions: read
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Harden runner
|
|
25
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
26
|
+
with:
|
|
27
|
+
egress-policy: audit
|
|
28
|
+
disable-sudo: true
|
|
29
|
+
|
|
30
|
+
- name: Checkout
|
|
31
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
show-progress: false
|
|
35
|
+
|
|
36
|
+
- name: Run analysis
|
|
37
|
+
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
|
38
|
+
with:
|
|
39
|
+
results_file: results.sarif
|
|
40
|
+
results_format: sarif
|
|
41
|
+
publish_results: true
|
|
42
|
+
|
|
43
|
+
- name: Upload to code-scanning
|
|
44
|
+
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
|
45
|
+
with:
|
|
46
|
+
sarif_file: results.sarif
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Zizmor
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 7 * * *"
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
zizmor:
|
|
14
|
+
name: Lint workflows
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
permissions:
|
|
18
|
+
security-events: write
|
|
19
|
+
contents: read
|
|
20
|
+
actions: read
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Harden runner
|
|
24
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
25
|
+
with:
|
|
26
|
+
egress-policy: audit
|
|
27
|
+
disable-sudo: true
|
|
28
|
+
|
|
29
|
+
- name: Checkout
|
|
30
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
31
|
+
with:
|
|
32
|
+
persist-credentials: false
|
|
33
|
+
show-progress: false
|
|
34
|
+
|
|
35
|
+
- name: Run zizmor
|
|
36
|
+
uses: zizmorcore/zizmor-action@b572f7b1a1c2d41efaab43d504f68d215c3cd727 # v0.5.4
|
|
37
|
+
with:
|
|
38
|
+
version: v1.25.0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
This project is generated by `create-daloy`. Apply security fixes to the
|
|
6
|
+
currently deployed branch and keep dependencies current through Dependabot.
|
|
7
|
+
|
|
8
|
+
## Reporting a vulnerability
|
|
9
|
+
|
|
10
|
+
Do not open a public issue for suspected vulnerabilities. Use your company's
|
|
11
|
+
private intake process, GitHub private vulnerability reporting, or the security
|
|
12
|
+
contact for this repository.
|
|
13
|
+
|
|
14
|
+
## Generated controls
|
|
15
|
+
|
|
16
|
+
The `--with-ci` bundle adds these defaults:
|
|
17
|
+
|
|
18
|
+
- GitHub Actions workflows use top-level `permissions: {}` and job-scoped permissions.
|
|
19
|
+
- Fork pull requests use `pull_request`, not `pull_request_target`.
|
|
20
|
+
- Third-party actions are pinned to commit SHAs.
|
|
21
|
+
- `actions/checkout` uses `persist-credentials: false`.
|
|
22
|
+
- Dependency installs run with lifecycle scripts disabled.
|
|
23
|
+
- Package-manager caches are disabled in CI to avoid cache-poisoning bridges.
|
|
24
|
+
- Lockfile source verification rejects git dependencies and non-registry tarball URLs.
|
|
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.
|
|
27
|
+
|
|
28
|
+
## Required repository settings
|
|
29
|
+
|
|
30
|
+
Before relying on these files for a company project:
|
|
31
|
+
|
|
32
|
+
1. Replace `@your-org/security-team` in `.github/CODEOWNERS` or pass `--code-owner` when scaffolding.
|
|
33
|
+
2. Protect the `main` branch and require the CI, CodeQL, Scorecard, and zizmor checks.
|
|
34
|
+
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.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Replace the placeholder owner with the GitHub user or team that must review
|
|
2
|
+
# privileged files before relying on CODEOWNERS protection.
|
|
3
|
+
|
|
4
|
+
* __CODE_OWNER__
|
|
5
|
+
|
|
6
|
+
# Workflow / CI / CD.
|
|
7
|
+
/.github/ __CODE_OWNER__
|
|
8
|
+
/.github/workflows/ __CODE_OWNER__
|
|
9
|
+
/.github/workflows/release.yml __CODE_OWNER__
|
|
10
|
+
/.github/dependabot.yml __CODE_OWNER__
|
|
11
|
+
/.github/CODEOWNERS __CODE_OWNER__
|
|
12
|
+
|
|
13
|
+
# Files that affect dependency resolution, install scripts, or publishing.
|
|
14
|
+
/package.json __CODE_OWNER__
|
|
15
|
+
/package-lock.json __CODE_OWNER__
|
|
16
|
+
/npm-shrinkwrap.json __CODE_OWNER__
|
|
17
|
+
/pnpm-lock.yaml __CODE_OWNER__
|
|
18
|
+
/pnpm-workspace.yaml __CODE_OWNER__
|
|
19
|
+
/yarn.lock __CODE_OWNER__
|
|
20
|
+
/bun.lock __CODE_OWNER__
|
|
21
|
+
/.npmrc __CODE_OWNER__
|
|
22
|
+
/scripts/verify-lockfile-sources.mjs __CODE_OWNER__
|
|
23
|
+
|
|
24
|
+
# Vulnerability handling.
|
|
25
|
+
/SECURITY.md __CODE_OWNER__
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
updates:
|
|
4
|
+
- package-ecosystem: github-actions
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: weekly
|
|
8
|
+
day: monday
|
|
9
|
+
open-pull-requests-limit: 10
|
|
10
|
+
groups:
|
|
11
|
+
actions:
|
|
12
|
+
patterns:
|
|
13
|
+
- "*"
|
|
14
|
+
commit-message:
|
|
15
|
+
prefix: "chore(actions)"
|
|
16
|
+
|
|
17
|
+
- package-ecosystem: npm
|
|
18
|
+
directory: "/"
|
|
19
|
+
schedule:
|
|
20
|
+
interval: weekly
|
|
21
|
+
day: monday
|
|
22
|
+
open-pull-requests-limit: 10
|
|
23
|
+
versioning-strategy: increase-if-necessary
|
|
24
|
+
groups:
|
|
25
|
+
dev-dependencies:
|
|
26
|
+
dependency-type: development
|
|
27
|
+
commit-message:
|
|
28
|
+
prefix: "chore(deps)"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Hardened CI generated by create-daloy --with-ci.
|
|
2
|
+
name: CI
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
verify:
|
|
18
|
+
name: Verify
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 20
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Harden runner
|
|
26
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
27
|
+
with:
|
|
28
|
+
egress-policy: audit
|
|
29
|
+
disable-sudo: true
|
|
30
|
+
disable-file-monitoring: false
|
|
31
|
+
|
|
32
|
+
- name: Checkout
|
|
33
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
34
|
+
with:
|
|
35
|
+
persist-credentials: false
|
|
36
|
+
show-progress: false
|
|
37
|
+
|
|
38
|
+
__SETUP_PACKAGE_MANAGER_STEP__
|
|
39
|
+
|
|
40
|
+
- name: Set up Node.js
|
|
41
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
42
|
+
with:
|
|
43
|
+
node-version: 24
|
|
44
|
+
# Package-manager caching is intentionally disabled. Shared caches
|
|
45
|
+
# can bridge fork PRs into trusted branches when mis-keyed.
|
|
46
|
+
|
|
47
|
+
__SETUP_BUN_RUNTIME_STEP__
|
|
48
|
+
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: __INSTALL_COMMAND__
|
|
51
|
+
env:
|
|
52
|
+
npm_config_ignore_scripts: "true"
|
|
53
|
+
|
|
54
|
+
- name: Verify lockfile sources
|
|
55
|
+
run: __VERIFY_LOCKFILE_COMMAND__
|
|
56
|
+
|
|
57
|
+
- name: Typecheck
|
|
58
|
+
run: __TYPECHECK_COMMAND__
|
|
59
|
+
|
|
60
|
+
- name: Test
|
|
61
|
+
run: __TEST_COMMAND__
|
|
62
|
+
|
|
63
|
+
__BUILD_STEP__
|
|
64
|
+
|
|
65
|
+
__AUDIT_STEP__
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "37 4 * * 1"
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
analyze:
|
|
15
|
+
name: Analyze (${{ matrix.language }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 30
|
|
18
|
+
permissions:
|
|
19
|
+
security-events: write
|
|
20
|
+
packages: read
|
|
21
|
+
actions: read
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
include:
|
|
28
|
+
- language: javascript-typescript
|
|
29
|
+
build-mode: none
|
|
30
|
+
- language: actions
|
|
31
|
+
build-mode: none
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- name: Harden runner
|
|
35
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
36
|
+
with:
|
|
37
|
+
egress-policy: audit
|
|
38
|
+
disable-sudo: true
|
|
39
|
+
|
|
40
|
+
- name: Checkout
|
|
41
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
42
|
+
with:
|
|
43
|
+
persist-credentials: false
|
|
44
|
+
show-progress: false
|
|
45
|
+
|
|
46
|
+
- name: Initialize CodeQL
|
|
47
|
+
uses: github/codeql-action/init@52485aec7be33610227643b0fe83936b8b5f061a # v3
|
|
48
|
+
with:
|
|
49
|
+
languages: ${{ matrix.language }}
|
|
50
|
+
build-mode: ${{ matrix.build-mode }}
|
|
51
|
+
queries: security-extended,security-and-quality
|
|
52
|
+
|
|
53
|
+
- name: Perform CodeQL Analysis
|
|
54
|
+
uses: github/codeql-action/analyze@52485aec7be33610227643b0fe83936b8b5f061a # v3
|
|
55
|
+
with:
|
|
56
|
+
category: "/language:${{ matrix.language }}"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Hardened npm publish skeleton generated by create-daloy --with-ci.
|
|
2
|
+
#
|
|
3
|
+
# Publishing is disabled until you set the repository variable
|
|
4
|
+
# NPM_PUBLISH_ENABLED=true, remove "private": true from package.json, and
|
|
5
|
+
# configure npm trusted publishing for this repository/environment.
|
|
6
|
+
name: Publish
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags:
|
|
11
|
+
- "v*"
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions: {}
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: publish-${{ github.ref }}
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
verify:
|
|
22
|
+
name: Verify before publish
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 20
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
steps:
|
|
28
|
+
- name: Harden runner
|
|
29
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
30
|
+
with:
|
|
31
|
+
egress-policy: audit
|
|
32
|
+
disable-sudo: true
|
|
33
|
+
|
|
34
|
+
- name: Checkout
|
|
35
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
36
|
+
with:
|
|
37
|
+
persist-credentials: false
|
|
38
|
+
show-progress: false
|
|
39
|
+
|
|
40
|
+
__SETUP_PACKAGE_MANAGER_STEP__
|
|
41
|
+
|
|
42
|
+
- name: Set up Node.js
|
|
43
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
44
|
+
with:
|
|
45
|
+
node-version: 24
|
|
46
|
+
|
|
47
|
+
__SETUP_BUN_RUNTIME_STEP__
|
|
48
|
+
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: __INSTALL_COMMAND__
|
|
51
|
+
env:
|
|
52
|
+
npm_config_ignore_scripts: "true"
|
|
53
|
+
|
|
54
|
+
- name: Verify lockfile sources
|
|
55
|
+
run: __VERIFY_LOCKFILE_COMMAND__
|
|
56
|
+
|
|
57
|
+
- name: Typecheck
|
|
58
|
+
run: __TYPECHECK_COMMAND__
|
|
59
|
+
|
|
60
|
+
- name: Test
|
|
61
|
+
run: __TEST_COMMAND__
|
|
62
|
+
|
|
63
|
+
__BUILD_STEP__
|
|
64
|
+
|
|
65
|
+
publish-npm:
|
|
66
|
+
name: Publish npm package
|
|
67
|
+
needs: verify
|
|
68
|
+
if: vars.NPM_PUBLISH_ENABLED == 'true'
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
timeout-minutes: 15
|
|
71
|
+
environment:
|
|
72
|
+
name: ${{ vars.NPM_PUBLISH_ENVIRONMENT || 'npm-publish' }}
|
|
73
|
+
url: https://www.npmjs.com/package/${{ github.event.repository.name }}
|
|
74
|
+
permissions:
|
|
75
|
+
contents: read
|
|
76
|
+
id-token: write
|
|
77
|
+
|
|
78
|
+
steps:
|
|
79
|
+
- name: Harden runner
|
|
80
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
81
|
+
with:
|
|
82
|
+
egress-policy: block
|
|
83
|
+
allowed-endpoints: >
|
|
84
|
+
api.github.com:443
|
|
85
|
+
github.com:443
|
|
86
|
+
objects.githubusercontent.com:443
|
|
87
|
+
registry.npmjs.org:443
|
|
88
|
+
fulcio.sigstore.dev:443
|
|
89
|
+
rekor.sigstore.dev:443
|
|
90
|
+
tuf-repo-cdn.sigstore.dev:443
|
|
91
|
+
disable-sudo: true
|
|
92
|
+
|
|
93
|
+
- name: Checkout
|
|
94
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
95
|
+
with:
|
|
96
|
+
persist-credentials: false
|
|
97
|
+
show-progress: false
|
|
98
|
+
|
|
99
|
+
__SETUP_PACKAGE_MANAGER_STEP__
|
|
100
|
+
|
|
101
|
+
- name: Set up Node.js
|
|
102
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
103
|
+
with:
|
|
104
|
+
node-version: 24
|
|
105
|
+
registry-url: "https://registry.npmjs.org"
|
|
106
|
+
|
|
107
|
+
__SETUP_BUN_RUNTIME_STEP__
|
|
108
|
+
|
|
109
|
+
- name: Install dependencies
|
|
110
|
+
run: __INSTALL_COMMAND__
|
|
111
|
+
env:
|
|
112
|
+
npm_config_ignore_scripts: "true"
|
|
113
|
+
|
|
114
|
+
- name: Refuse private package publish
|
|
115
|
+
run: |
|
|
116
|
+
node -e "const pkg = require('./package.json'); if (pkg.private) { console.error('package.json is private; remove private:true before enabling npm publish'); process.exit(1); }"
|
|
117
|
+
|
|
118
|
+
__TAG_VERSION_CHECK_STEP__
|
|
119
|
+
|
|
120
|
+
__BUILD_STEP__
|
|
121
|
+
|
|
122
|
+
- name: Publish to npm with provenance
|
|
123
|
+
run: npm publish --access public --provenance
|
|
124
|
+
env:
|
|
125
|
+
NPM_CONFIG_PROVENANCE: "true"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Scorecard
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
branch_protection_rule:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: "23 5 * * 2"
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
analysis:
|
|
14
|
+
name: Scorecard analysis
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 15
|
|
17
|
+
permissions:
|
|
18
|
+
security-events: write
|
|
19
|
+
id-token: write
|
|
20
|
+
contents: read
|
|
21
|
+
actions: read
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Harden runner
|
|
25
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
26
|
+
with:
|
|
27
|
+
egress-policy: audit
|
|
28
|
+
disable-sudo: true
|
|
29
|
+
|
|
30
|
+
- name: Checkout
|
|
31
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
show-progress: false
|
|
35
|
+
|
|
36
|
+
- name: Run analysis
|
|
37
|
+
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
|
38
|
+
with:
|
|
39
|
+
results_file: results.sarif
|
|
40
|
+
results_format: sarif
|
|
41
|
+
publish_results: true
|
|
42
|
+
|
|
43
|
+
- name: Upload to code-scanning
|
|
44
|
+
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
|
45
|
+
with:
|
|
46
|
+
sarif_file: results.sarif
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Zizmor
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 7 * * *"
|
|
9
|
+
|
|
10
|
+
permissions: {}
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
zizmor:
|
|
14
|
+
name: Lint workflows
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
permissions:
|
|
18
|
+
security-events: write
|
|
19
|
+
contents: read
|
|
20
|
+
actions: read
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Harden runner
|
|
24
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
25
|
+
with:
|
|
26
|
+
egress-policy: audit
|
|
27
|
+
disable-sudo: true
|
|
28
|
+
|
|
29
|
+
- name: Checkout
|
|
30
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
31
|
+
with:
|
|
32
|
+
persist-credentials: false
|
|
33
|
+
show-progress: false
|
|
34
|
+
|
|
35
|
+
- name: Run zizmor
|
|
36
|
+
uses: zizmorcore/zizmor-action@b572f7b1a1c2d41efaab43d504f68d215c3cd727 # v0.5.4
|
|
37
|
+
with:
|
|
38
|
+
version: v1.25.0
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const TEXT_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lock"];
|
|
4
|
+
const BINARY_LOCKFILES = ["bun.lockb"];
|
|
5
|
+
const GIT_SOURCE_PATTERN = /(?:specifier:\s*)?(?:github:|git\+|git:\/\/|ssh:\/\/git@|git@github\.com:)/i;
|
|
6
|
+
const URL_PATTERN = /(?:tarball|resolved)["':\s]+(?<url>https?:\/\/[^"'\s},]+)/i;
|
|
7
|
+
const ALLOWED_TARBALL_PREFIXES = ["https://registry.npmjs.org/", "https://registry.yarnpkg.com/"];
|
|
8
|
+
|
|
9
|
+
async function exists(file) {
|
|
10
|
+
try {
|
|
11
|
+
await access(file);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findForbiddenSources(lockfile) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
const lines = lockfile.split(/\r?\n/);
|
|
21
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
22
|
+
const text = rawLine.trim();
|
|
23
|
+
if (GIT_SOURCE_PATTERN.test(text)) {
|
|
24
|
+
findings.push({ line: index + 1, reason: "git dependency source", text });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = URL_PATTERN.exec(text)?.groups?.url;
|
|
29
|
+
if (url && !ALLOWED_TARBALL_PREFIXES.some((prefix) => url.startsWith(prefix))) {
|
|
30
|
+
findings.push({ line: index + 1, reason: "non-registry tarball source", text });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return findings;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let checked = false;
|
|
37
|
+
let failed = false;
|
|
38
|
+
|
|
39
|
+
for (const file of TEXT_LOCKFILES) {
|
|
40
|
+
if (!(await exists(file))) continue;
|
|
41
|
+
checked = true;
|
|
42
|
+
const findings = findForbiddenSources(await readFile(file, "utf8"));
|
|
43
|
+
for (const finding of findings) {
|
|
44
|
+
failed = true;
|
|
45
|
+
console.error(`${file}: ${finding.reason} on line ${finding.line}: ${finding.text}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const file of BINARY_LOCKFILES) {
|
|
50
|
+
if (await exists(file)) {
|
|
51
|
+
failed = true;
|
|
52
|
+
console.error(`${file}: binary lockfiles cannot be source-verified; use a text lockfile in CI.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!checked) {
|
|
57
|
+
failed = true;
|
|
58
|
+
console.error("No text lockfile found. Commit a lockfile before relying on CI.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (failed) process.exitCode = 1;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# pnpm workspace-level supply-chain settings.
|
|
2
|
+
#
|
|
3
|
+
# This file is here even though the default package manager for this
|
|
4
|
+
# template is Bun, because users can switch to pnpm with
|
|
5
|
+
# `pnpm create daloy --package-manager pnpm`. When that happens, the
|
|
6
|
+
# scaffolder keeps this file so pnpm 11+ picks up the workspace-level
|
|
7
|
+
# supply-chain controls below. If you keep using Bun, you can delete it.
|
|
8
|
+
#
|
|
9
|
+
# pnpm 11+ reads `pnpm-workspace.yaml` as a settings file even when no
|
|
10
|
+
# `packages:` list is declared. It is the only file format that accepts
|
|
11
|
+
# the v11 keys below (`.npmrc` cannot express them).
|
|
12
|
+
#
|
|
13
|
+
# See the DaloyJS supply-chain notes and the 2026-05-11 TanStack
|
|
14
|
+
# postmortem (https://tanstack.com/blog/npm-supply-chain-compromise-postmortem)
|
|
15
|
+
# for why every setting below is on by default.
|
|
16
|
+
|
|
17
|
+
# Wait 24h (1440 minutes) before resolving a freshly published version.
|
|
18
|
+
# Most npm worm campaigns are detected and unpublished within hours.
|
|
19
|
+
# Set to 0 only for a real hotfix.
|
|
20
|
+
minimumReleaseAge: 1440
|
|
21
|
+
|
|
22
|
+
# Only direct dependencies may use exotic sources (git, tarball URLs).
|
|
23
|
+
# Transitive deps must resolve from the configured registry, which makes
|
|
24
|
+
# typosquatted-tarball and compromised-fork attacks much harder to land.
|
|
25
|
+
blockExoticSubdeps: true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# pnpm workspace-level supply-chain settings.
|
|
2
|
+
#
|
|
3
|
+
# This file looks unusual in a single-package project, but pnpm 11+ reads
|
|
4
|
+
# `pnpm-workspace.yaml` as a settings file even when no `packages:` list
|
|
5
|
+
# is declared. It is the only file format that accepts the v11 keys
|
|
6
|
+
# below (`.npmrc` cannot express them).
|
|
7
|
+
#
|
|
8
|
+
# See the DaloyJS supply-chain notes and the 2026-05-11 TanStack
|
|
9
|
+
# postmortem (https://tanstack.com/blog/npm-supply-chain-compromise-postmortem)
|
|
10
|
+
# for why every setting below is on by default.
|
|
11
|
+
#
|
|
12
|
+
# Want a real monorepo? Add a `packages:` key listing your sub-package
|
|
13
|
+
# globs (e.g. `packages: ["apps/*", "packages/*"]`) and the keys below
|
|
14
|
+
# will continue to apply.
|
|
15
|
+
|
|
16
|
+
# Wait 24h (1440 minutes) before resolving a freshly published version.
|
|
17
|
+
# Most npm worm campaigns are detected and unpublished within hours.
|
|
18
|
+
# Set to 0 only for a real hotfix.
|
|
19
|
+
minimumReleaseAge: 1440
|
|
20
|
+
|
|
21
|
+
# Only direct dependencies may use exotic sources (git, tarball URLs).
|
|
22
|
+
# Transitive deps must resolve from the configured registry, which makes
|
|
23
|
+
# typosquatted-tarball and compromised-fork attacks much harder to land.
|
|
24
|
+
blockExoticSubdeps: true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# pnpm workspace-level supply-chain settings.
|
|
2
|
+
#
|
|
3
|
+
# This file looks unusual in a single-package project, but pnpm 11+ reads
|
|
4
|
+
# `pnpm-workspace.yaml` as a settings file even when no `packages:` list
|
|
5
|
+
# is declared. It is the only file format that accepts the v11 keys
|
|
6
|
+
# below (`.npmrc` cannot express them).
|
|
7
|
+
#
|
|
8
|
+
# See the DaloyJS supply-chain notes and the 2026-05-11 TanStack
|
|
9
|
+
# postmortem (https://tanstack.com/blog/npm-supply-chain-compromise-postmortem)
|
|
10
|
+
# for why every setting below is on by default.
|
|
11
|
+
#
|
|
12
|
+
# Want a real monorepo? Add a `packages:` key listing your sub-package
|
|
13
|
+
# globs (e.g. `packages: ["apps/*", "packages/*"]`) and the keys below
|
|
14
|
+
# will continue to apply.
|
|
15
|
+
|
|
16
|
+
# Wait 24h (1440 minutes) before resolving a freshly published version.
|
|
17
|
+
# Most npm worm campaigns are detected and unpublished within hours.
|
|
18
|
+
# Set to 0 only for a real hotfix.
|
|
19
|
+
minimumReleaseAge: 1440
|
|
20
|
+
|
|
21
|
+
# Only direct dependencies may use exotic sources (git, tarball URLs).
|
|
22
|
+
# Transitive deps must resolve from the configured registry, which makes
|
|
23
|
+
# typosquatted-tarball and compromised-fork attacks much harder to land.
|
|
24
|
+
blockExoticSubdeps: true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# pnpm workspace-level supply-chain settings.
|
|
2
|
+
#
|
|
3
|
+
# This file looks unusual in a single-package project, but pnpm 11+ reads
|
|
4
|
+
# `pnpm-workspace.yaml` as a settings file even when no `packages:` list
|
|
5
|
+
# is declared. It is the only file format that accepts the v11 keys
|
|
6
|
+
# below (`.npmrc` cannot express them).
|
|
7
|
+
#
|
|
8
|
+
# See the DaloyJS supply-chain notes and the 2026-05-11 TanStack
|
|
9
|
+
# postmortem (https://tanstack.com/blog/npm-supply-chain-compromise-postmortem)
|
|
10
|
+
# for why every setting below is on by default.
|
|
11
|
+
#
|
|
12
|
+
# Want a real monorepo? Add a `packages:` key listing your sub-package
|
|
13
|
+
# globs (e.g. `packages: ["apps/*", "packages/*"]`) and the keys below
|
|
14
|
+
# will continue to apply.
|
|
15
|
+
|
|
16
|
+
# Wait 24h (1440 minutes) before resolving a freshly published version.
|
|
17
|
+
# Most npm worm campaigns are detected and unpublished within hours.
|
|
18
|
+
# Set to 0 only for a real hotfix.
|
|
19
|
+
minimumReleaseAge: 1440
|
|
20
|
+
|
|
21
|
+
# Only direct dependencies may use exotic sources (git, tarball URLs).
|
|
22
|
+
# Transitive deps must resolve from the configured registry, which makes
|
|
23
|
+
# typosquatted-tarball and compromised-fork attacks much harder to land.
|
|
24
|
+
blockExoticSubdeps: true
|