create-daloy 0.34.2 → 0.34.3
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 +15 -2
- package/bin/create-daloy.mjs +267 -14
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/_ci/deno/_github/workflows/deploy.yml +118 -0
- package/templates/_ci/node/SECURITY.md +1 -0
- package/templates/_ci/node/_github/CODEOWNERS +1 -0
- package/templates/_ci/node/_github/workflows/dast.yml +177 -0
- package/templates/_ci/node/_github/workflows/deploy.yml +94 -0
- package/templates/bun-basic/package.json +1 -1
- package/templates/cloudflare-worker/package.json +1 -1
- package/templates/deno-basic/deno.json +2 -2
- package/templates/node-basic/package.json +1 -1
- package/templates/vercel-edge/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ pnpm create daloy@latest my-api \
|
|
|
57
57
|
| `--git` / `--no-git` | Initialize a git repository. Defaults to interactive. |
|
|
58
58
|
| `--minimal` | Strip the bookstore demo route and the built-in `/docs` + `/openapi.json` routes so only the framework bootstrap and `/healthz` ship. |
|
|
59
59
|
| `--with-ci` / `--no-ci` | Add the hardened GitHub Actions, Dependabot, CODEOWNERS, SECURITY.md, and lockfile-source verification bundle. **Defaults to Y** so scaffolded projects are secure by default. |
|
|
60
|
+
| `--with-deploy` / `--no-deploy` | Add the starter `.github/workflows/deploy.yml`. Defaults to the same value as `--with-ci`, so you can keep CI but opt out of deploy scaffolding with `--no-deploy`. |
|
|
60
61
|
| `--code-owner <owner>` | Replace the CODEOWNERS placeholder when `--with-ci` is used, for example `@acme/security`. |
|
|
61
62
|
| `--force` | Overwrite an existing non-empty directory. |
|
|
62
63
|
| `--yes` | Accept all defaults; never prompt. |
|
|
@@ -153,6 +154,12 @@ For Node-style templates, the bundle adds:
|
|
|
153
154
|
- `.github/workflows/ci.yml` with top-level `permissions: {}`, pinned actions,
|
|
154
155
|
`harden-runner`, `persist-credentials: false`, no package-manager cache, and
|
|
155
156
|
install scripts disabled.
|
|
157
|
+
- `.github/workflows/deploy.yml` as a manual-only deployment starter. Container
|
|
158
|
+
templates publish a Docker image to GHCR with the repo-scoped `GITHUB_TOKEN`,
|
|
159
|
+
while Vercel and Cloudflare templates ship concrete CLI deploy steps that
|
|
160
|
+
read their platform credentials from GitHub Actions secrets/variables. The
|
|
161
|
+
deploy job is gated to `main` or a tag by default, and Node-style templates
|
|
162
|
+
re-run `verify:lockfile` before shipping.
|
|
156
163
|
- `.github/workflows/vuln-scan.yml` — a daily scheduled SCA cron that runs the
|
|
157
164
|
package manager's audit against the committed lockfile. Catches CVEs disclosed
|
|
158
165
|
*after* the last PR or push and provides SOC 2 CC7.1
|
|
@@ -166,8 +173,14 @@ The bundle deliberately does **not** generate an npm publish workflow.
|
|
|
166
173
|
`create-daloy` scaffolds REST API services, not libraries; if you later carve
|
|
167
174
|
out a reusable package, opt into npm trusted publishing yourself.
|
|
168
175
|
|
|
169
|
-
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow
|
|
170
|
-
|
|
176
|
+
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow, a manual-only
|
|
177
|
+
container publish starter for GHCR that is guarded to `main` or a tag by
|
|
178
|
+
default, plus CodeQL, Scorecard, zizmor, Dependabot for GitHub Actions,
|
|
179
|
+
CODEOWNERS, and `SECURITY.md`.
|
|
180
|
+
|
|
181
|
+
If you want the governance bundle but not the deployment starter, pass
|
|
182
|
+
`--with-ci --no-deploy`. If you only want a deployment starter, pass
|
|
183
|
+
`--with-deploy --no-ci`.
|
|
171
184
|
|
|
172
185
|
If you omit `--code-owner`, the generated CODEOWNERS file uses
|
|
173
186
|
`@your-org/security-team` as a placeholder. Replace it before relying on branch
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -333,6 +333,7 @@ ${heading("Options")}
|
|
|
333
333
|
${color(COLORS.green, "--git / --no-git")} Initialize a git repository.
|
|
334
334
|
${color(COLORS.green, "--minimal")} Strip the bookstore + OpenAPI docs demo routes.
|
|
335
335
|
${color(COLORS.green, "--with-ci / --no-ci")} Add hardened GitHub Actions + governance files. ${color(COLORS.dim, "(default: Y)")}
|
|
336
|
+
${color(COLORS.green, "--with-deploy / --no-deploy")} Add starter deploy.yml workflow(s). ${color(COLORS.dim, "(default: inherits --with-ci)")}
|
|
336
337
|
${color(COLORS.green, "--code-owner <owner>")} CODEOWNERS owner for --with-ci, e.g. @acme/security.
|
|
337
338
|
${color(COLORS.green, "--force")} Overwrite an existing non-empty directory.
|
|
338
339
|
${color(COLORS.green, "--yes, -y")} Accept all defaults; never prompt.
|
|
@@ -380,6 +381,7 @@ function parseArgs(argv) {
|
|
|
380
381
|
listTemplates: false,
|
|
381
382
|
minimal: false,
|
|
382
383
|
ci: undefined,
|
|
384
|
+
deploy: undefined,
|
|
383
385
|
codeOwner: undefined,
|
|
384
386
|
};
|
|
385
387
|
const args = [...argv];
|
|
@@ -393,6 +395,8 @@ function parseArgs(argv) {
|
|
|
393
395
|
else if (a === "--minimal") out.minimal = true;
|
|
394
396
|
else if (a === "--with-ci") out.ci = true;
|
|
395
397
|
else if (a === "--no-ci") out.ci = false;
|
|
398
|
+
else if (a === "--with-deploy") out.deploy = true;
|
|
399
|
+
else if (a === "--no-deploy") out.deploy = false;
|
|
396
400
|
else if (a === "--code-owner") out.codeOwner = args.shift();
|
|
397
401
|
else if (a?.startsWith("--code-owner=")) out.codeOwner = a.slice("--code-owner=".length);
|
|
398
402
|
else if (a === "--install") out.install = true;
|
|
@@ -686,6 +690,15 @@ function runScriptCommand(packageManager, scriptName) {
|
|
|
686
690
|
return `${packageManager} run ${scriptName}`;
|
|
687
691
|
}
|
|
688
692
|
|
|
693
|
+
function execCommand(packageManager, binary, args = "") {
|
|
694
|
+
const suffix = args ? ` ${args}` : "";
|
|
695
|
+
if (packageManager === "pnpm") return `pnpm exec ${binary}${suffix}`;
|
|
696
|
+
if (packageManager === "npm") return `npx ${binary}${suffix}`;
|
|
697
|
+
if (packageManager === "yarn") return `yarn exec ${binary}${suffix}`;
|
|
698
|
+
if (packageManager === "bun") return `bunx ${binary}${suffix}`;
|
|
699
|
+
return `${binary}${suffix}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
689
702
|
function installCommand(packageManager) {
|
|
690
703
|
if (packageManager === "pnpm") return "pnpm install --frozen-lockfile --ignore-scripts";
|
|
691
704
|
if (packageManager === "npm") return "npm ci --ignore-scripts";
|
|
@@ -740,6 +753,163 @@ function setupBunStep() {
|
|
|
740
753
|
bun-version: latest`;
|
|
741
754
|
}
|
|
742
755
|
|
|
756
|
+
function setupNodeStep() {
|
|
757
|
+
return ` - name: Set up Node.js
|
|
758
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
759
|
+
with:
|
|
760
|
+
node-version: 24
|
|
761
|
+
# Package-manager caching is intentionally disabled. Shared caches
|
|
762
|
+
# can bridge fork PRs into trusted branches when mis-keyed.`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function installDependenciesStep(command) {
|
|
766
|
+
return ` - name: Install dependencies
|
|
767
|
+
run: ${command}
|
|
768
|
+
env:
|
|
769
|
+
npm_config_ignore_scripts: "true"`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function joinWorkflowBlocks(blocks) {
|
|
773
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Deploy job ref guard. `workflow_dispatch` accepts any branch by default, so
|
|
777
|
+
// a junior could trigger a production deploy from an unreviewed feature
|
|
778
|
+
// branch by accident. This `if:` limits the deploy job to `main` or a tag
|
|
779
|
+
// push while still allowing maintainers to flip the guard off if they need
|
|
780
|
+
// to deploy from a release branch.
|
|
781
|
+
function deployRefGuard() {
|
|
782
|
+
return ` # Lock production deploys to main or a tag push. \`workflow_dispatch\`
|
|
783
|
+
# accepts any branch, so this guard stops an accidental dispatch from a
|
|
784
|
+
# feature branch from shipping unreviewed code.
|
|
785
|
+
if: github.ref == 'refs/heads/main' || github.ref_type == 'tag'`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function containerRegistryHeader() {
|
|
789
|
+
return `# Default target: GitHub Container Registry (GHCR).
|
|
790
|
+
# No extra secret is required; the workflow uses the repo-scoped GITHUB_TOKEN.
|
|
791
|
+
# After the image is published, point your platform at GHCR or replace the
|
|
792
|
+
# final push steps with your provider's deploy command.
|
|
793
|
+
#
|
|
794
|
+
# Optional container-host handoff examples:
|
|
795
|
+
# - Fly.io: install flyctl in a later step and run
|
|
796
|
+
# flyctl deploy --image "$IMAGE_REPO:sha-\${GITHUB_SHA}" --remote-only
|
|
797
|
+
# - Render: create an image-backed service that tracks
|
|
798
|
+
# ghcr.io/<owner>/<repo>:sha-\${GITHUB_SHA}`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function containerPublishSteps() {
|
|
802
|
+
return ` - name: Log in to GHCR
|
|
803
|
+
env:
|
|
804
|
+
GH_TOKEN: \${{ github.token }}
|
|
805
|
+
run: |
|
|
806
|
+
set -eu
|
|
807
|
+
echo "$GH_TOKEN" | docker login ghcr.io -u "\${{ github.actor }}" --password-stdin
|
|
808
|
+
|
|
809
|
+
- name: Derive image name
|
|
810
|
+
run: |
|
|
811
|
+
set -eu
|
|
812
|
+
owner="\${GITHUB_REPOSITORY_OWNER,,}"
|
|
813
|
+
repo="\${GITHUB_REPOSITORY#*/}"
|
|
814
|
+
repo="\${repo,,}"
|
|
815
|
+
echo "IMAGE_REPO=ghcr.io/\${owner}/\${repo}" >> "$GITHUB_ENV"
|
|
816
|
+
|
|
817
|
+
- name: Build image
|
|
818
|
+
env:
|
|
819
|
+
DOCKER_BUILDKIT: "1"
|
|
820
|
+
run: |
|
|
821
|
+
set -eu
|
|
822
|
+
docker build \
|
|
823
|
+
--tag "$IMAGE_REPO:sha-\${GITHUB_SHA}" \
|
|
824
|
+
--tag "$IMAGE_REPO:latest" \
|
|
825
|
+
.
|
|
826
|
+
|
|
827
|
+
- name: Push image
|
|
828
|
+
run: |
|
|
829
|
+
set -eu
|
|
830
|
+
docker push "$IMAGE_REPO:sha-\${GITHUB_SHA}"
|
|
831
|
+
docker push "$IMAGE_REPO:latest"`;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function vercelDeployHeader() {
|
|
835
|
+
return `# Configure before the first run:
|
|
836
|
+
# - GitHub Actions secret: VERCEL_TOKEN
|
|
837
|
+
# - GitHub Actions variables or environment variables: VERCEL_ORG_ID and VERCEL_PROJECT_ID
|
|
838
|
+
# This workflow stays manual-only until you wire those in and decide whether
|
|
839
|
+
# you want automatic deploys from main.`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function cloudflareDeployHeader() {
|
|
843
|
+
return `# Configure before the first run:
|
|
844
|
+
# - GitHub Actions secret: CLOUDFLARE_API_TOKEN
|
|
845
|
+
# - GitHub Actions variable or environment variable: CLOUDFLARE_ACCOUNT_ID
|
|
846
|
+
# Keep this manual-only until the worker is linked to the right account and
|
|
847
|
+
# the production environment has approval rules.`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function providerDeploySetup({ packageManager, needsBunRuntime }) {
|
|
851
|
+
return joinWorkflowBlocks([
|
|
852
|
+
setupPackageManagerStep(packageManager),
|
|
853
|
+
setupNodeStep(),
|
|
854
|
+
needsBunRuntime ? setupBunStep() : "",
|
|
855
|
+
installDependenciesStep(installCommand(packageManager)),
|
|
856
|
+
]);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function vercelDeploySteps(packageManager) {
|
|
860
|
+
return ` - name: Deploy to Vercel
|
|
861
|
+
env:
|
|
862
|
+
VERCEL_TOKEN: \${{ secrets.VERCEL_TOKEN }}
|
|
863
|
+
VERCEL_ORG_ID: \${{ vars.VERCEL_ORG_ID }}
|
|
864
|
+
VERCEL_PROJECT_ID: \${{ vars.VERCEL_PROJECT_ID }}
|
|
865
|
+
run: |
|
|
866
|
+
set -eu
|
|
867
|
+
: "\${VERCEL_TOKEN:?Set the VERCEL_TOKEN Actions secret before running this workflow.}"
|
|
868
|
+
: "\${VERCEL_ORG_ID:?Set the VERCEL_ORG_ID Actions variable before running this workflow.}"
|
|
869
|
+
: "\${VERCEL_PROJECT_ID:?Set the VERCEL_PROJECT_ID Actions variable before running this workflow.}"
|
|
870
|
+
${execCommand(packageManager, "vercel", "deploy --prod --yes --token \"$VERCEL_TOKEN\"")}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function cloudflareDeploySteps(packageManager) {
|
|
874
|
+
return ` - name: Deploy to Cloudflare Workers
|
|
875
|
+
env:
|
|
876
|
+
CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
877
|
+
CLOUDFLARE_ACCOUNT_ID: \${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
|
878
|
+
run: |
|
|
879
|
+
set -eu
|
|
880
|
+
: "\${CLOUDFLARE_API_TOKEN:?Set the CLOUDFLARE_API_TOKEN Actions secret before running this workflow.}"
|
|
881
|
+
: "\${CLOUDFLARE_ACCOUNT_ID:?Set the CLOUDFLARE_ACCOUNT_ID Actions variable before running this workflow.}"
|
|
882
|
+
${execCommand(packageManager, "wrangler", "deploy")}`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function renderDeployConfig({ template, packageManager, needsBunRuntime }) {
|
|
886
|
+
if (template === "vercel-edge") {
|
|
887
|
+
return {
|
|
888
|
+
header: vercelDeployHeader(),
|
|
889
|
+
jobName: "Deploy to Vercel",
|
|
890
|
+
jobPermissions: "",
|
|
891
|
+
setupSteps: providerDeploySetup({ packageManager, needsBunRuntime }),
|
|
892
|
+
steps: vercelDeploySteps(packageManager),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (template === "cloudflare-worker") {
|
|
896
|
+
return {
|
|
897
|
+
header: cloudflareDeployHeader(),
|
|
898
|
+
jobName: "Deploy to Cloudflare Workers",
|
|
899
|
+
jobPermissions: "",
|
|
900
|
+
setupSteps: providerDeploySetup({ packageManager, needsBunRuntime }),
|
|
901
|
+
steps: cloudflareDeploySteps(packageManager),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
header: containerRegistryHeader(),
|
|
906
|
+
jobName: "Publish container image",
|
|
907
|
+
jobPermissions: " packages: write",
|
|
908
|
+
setupSteps: "",
|
|
909
|
+
steps: containerPublishSteps(),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
743
913
|
function workflowStep(name, command) {
|
|
744
914
|
return ` - name: ${name}
|
|
745
915
|
run: ${command}`;
|
|
@@ -763,13 +933,21 @@ async function addLockfileVerifyScript(dir) {
|
|
|
763
933
|
await writePackageJson(dir, packageJson);
|
|
764
934
|
}
|
|
765
935
|
|
|
766
|
-
function renderCiReplacements({ packageManager, template, packageJson, codeOwner }) {
|
|
936
|
+
function renderCiReplacements({ packageManager, template, packageJson, codeOwner, includeSecurityBundle = true }) {
|
|
767
937
|
const setupPm = setupPackageManagerStep(packageManager);
|
|
768
938
|
const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
|
|
769
939
|
const audit = auditCommand(packageManager);
|
|
770
940
|
const auditFull = auditFullCommand(packageManager);
|
|
771
941
|
const buildStep = hasPackageScript(packageJson, "build") ? workflowStep("Build", runScriptCommand(packageManager, "build")) : "";
|
|
772
942
|
const auditStep = audit ? workflowStep(auditStepName(packageManager), audit) : "";
|
|
943
|
+
const deploy = renderDeployConfig({ template, packageManager, needsBunRuntime });
|
|
944
|
+
// The verify:lockfile script is only scaffolded when the security bundle
|
|
945
|
+
// ships. In a deploy-only scaffold the script does not exist on disk, so
|
|
946
|
+
// the deploy workflow must omit the step instead of failing fast on a
|
|
947
|
+
// missing file.
|
|
948
|
+
const deployVerifyLockfileStep = includeSecurityBundle
|
|
949
|
+
? workflowStep("Verify lockfile sources", runScriptCommand(packageManager, "verify:lockfile"))
|
|
950
|
+
: "";
|
|
773
951
|
// vuln-scan.yml: production-tree audit is blocking, full-tree audit is
|
|
774
952
|
// advisory (continue-on-error) so a low-severity dev-tool advisory does
|
|
775
953
|
// not page the on-call on a daily cron.
|
|
@@ -781,6 +959,13 @@ function renderCiReplacements({ packageManager, template, packageJson, codeOwner
|
|
|
781
959
|
: "";
|
|
782
960
|
|
|
783
961
|
return new Map([
|
|
962
|
+
["__DEPLOY_HEADER__", deploy.header],
|
|
963
|
+
["__DEPLOY_JOB_NAME__", deploy.jobName],
|
|
964
|
+
["__DEPLOY_JOB_PERMISSIONS__", deploy.jobPermissions],
|
|
965
|
+
["__DEPLOY_SETUP_STEPS__", deploy.setupSteps],
|
|
966
|
+
["__DEPLOY_STEPS__", deploy.steps],
|
|
967
|
+
["__DEPLOY_VERIFY_LOCKFILE_STEP__", deployVerifyLockfileStep],
|
|
968
|
+
["__DEPLOY_REF_GUARD__", deployRefGuard()],
|
|
784
969
|
["__CODE_OWNER__", codeOwner],
|
|
785
970
|
["__SETUP_PACKAGE_MANAGER_STEP__", setupPm],
|
|
786
971
|
["__SETUP_BUN_RUNTIME_STEP__", needsBunRuntime ? setupBunStep() : ""],
|
|
@@ -817,7 +1002,35 @@ async function replacePlaceholdersInTree(dir, replacements) {
|
|
|
817
1002
|
}
|
|
818
1003
|
}
|
|
819
1004
|
|
|
820
|
-
async function
|
|
1005
|
+
async function pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow }) {
|
|
1006
|
+
if (!includeSecurityBundle) {
|
|
1007
|
+
const workflowFiles = flavor === "deno"
|
|
1008
|
+
? ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "scorecard.yml", "zizmor.yml"]
|
|
1009
|
+
: ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "scorecard.yml", "vuln-scan.yml", "zizmor.yml"];
|
|
1010
|
+
for (const file of workflowFiles) {
|
|
1011
|
+
await rm(path.join(targetDir, ".github", "workflows", file), { force: true });
|
|
1012
|
+
}
|
|
1013
|
+
await rm(path.join(targetDir, ".github", "dependabot.yml"), { force: true });
|
|
1014
|
+
await rm(path.join(targetDir, ".github", "CODEOWNERS"), { force: true });
|
|
1015
|
+
await rm(path.join(targetDir, "SECURITY.md"), { force: true });
|
|
1016
|
+
if (flavor === "node") {
|
|
1017
|
+
await rm(path.join(targetDir, "scripts", "verify-lockfile-sources.mjs"), { force: true });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (!includeDeployWorkflow) {
|
|
1022
|
+
await rm(path.join(targetDir, ".github", "workflows", "deploy.yml"), { force: true });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function copyCiBundle(
|
|
1027
|
+
targetDir,
|
|
1028
|
+
template,
|
|
1029
|
+
packageManager,
|
|
1030
|
+
skipPackageManager,
|
|
1031
|
+
codeOwner,
|
|
1032
|
+
{ includeSecurityBundle = true, includeDeployWorkflow = true } = {},
|
|
1033
|
+
) {
|
|
821
1034
|
const flavor = skipPackageManager ? "deno" : "node";
|
|
822
1035
|
const sourceDir = path.join(CI_TEMPLATES_DIR, flavor);
|
|
823
1036
|
if (!existsSync(sourceDir)) {
|
|
@@ -825,23 +1038,42 @@ async function copyCiBundle(targetDir, template, packageManager, skipPackageMana
|
|
|
825
1038
|
}
|
|
826
1039
|
await copyTemplate(sourceDir, targetDir);
|
|
827
1040
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
);
|
|
1041
|
+
await pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow });
|
|
1042
|
+
|
|
1043
|
+
if (!includeSecurityBundle && !includeDeployWorkflow) {
|
|
1044
|
+
return;
|
|
833
1045
|
}
|
|
1046
|
+
|
|
1047
|
+
const candidate = codeOwner?.trim() ?? "";
|
|
834
1048
|
const owner = candidate || "@your-org/security-team";
|
|
835
1049
|
if (skipPackageManager) {
|
|
1050
|
+
if (includeSecurityBundle && candidate && !VALID_CODE_OWNER.test(candidate)) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
836
1055
|
await replacePlaceholdersInTree(targetDir, new Map([["__CODE_OWNER__", owner]]));
|
|
837
1056
|
return;
|
|
838
1057
|
}
|
|
839
1058
|
|
|
840
|
-
|
|
1059
|
+
if (includeSecurityBundle) {
|
|
1060
|
+
if (candidate && !VALID_CODE_OWNER.test(candidate)) {
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
await addLockfileVerifyScript(targetDir);
|
|
1066
|
+
}
|
|
841
1067
|
const packageJson = await readPackageJsonIfPresent(targetDir);
|
|
842
1068
|
await replacePlaceholdersInTree(
|
|
843
1069
|
targetDir,
|
|
844
|
-
renderCiReplacements({
|
|
1070
|
+
renderCiReplacements({
|
|
1071
|
+
packageManager,
|
|
1072
|
+
template,
|
|
1073
|
+
packageJson,
|
|
1074
|
+
codeOwner: owner,
|
|
1075
|
+
includeSecurityBundle,
|
|
1076
|
+
}),
|
|
845
1077
|
);
|
|
846
1078
|
}
|
|
847
1079
|
|
|
@@ -1213,7 +1445,7 @@ function createSpinner(initialMessage) {
|
|
|
1213
1445
|
};
|
|
1214
1446
|
}
|
|
1215
1447
|
|
|
1216
|
-
function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi }) {
|
|
1448
|
+
function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy }) {
|
|
1217
1449
|
const templateMeta = TEMPLATE_OPTIONS.find((option) => option.value === template);
|
|
1218
1450
|
const templateLabel = templateMeta ? `${templateMeta.title} ${color(COLORS.dim, `(${template})`)}` : template;
|
|
1219
1451
|
const summaryLines = [
|
|
@@ -1230,6 +1462,9 @@ function printSummary({ projectName, template, packageManager, installDeps, skip
|
|
|
1230
1462
|
if (withCi) {
|
|
1231
1463
|
summaryLines.push(`${color(COLORS.gray, "Security ")} ${color(COLORS.cyan, "GitHub CI bundle")}`);
|
|
1232
1464
|
}
|
|
1465
|
+
if (withDeploy) {
|
|
1466
|
+
summaryLines.push(`${color(COLORS.gray, "Deploy ")} ${color(COLORS.cyan, "Starter workflow")}`);
|
|
1467
|
+
}
|
|
1233
1468
|
console.log("");
|
|
1234
1469
|
console.log(renderBox(summaryLines, { accent: COLORS.green }));
|
|
1235
1470
|
console.log("");
|
|
@@ -1413,6 +1648,15 @@ async function main() {
|
|
|
1413
1648
|
withCi = rl ? await askYesNo(rl, "Add hardened GitHub Actions and security files?", true) : true;
|
|
1414
1649
|
}
|
|
1415
1650
|
|
|
1651
|
+
let withDeploy = opts.deploy;
|
|
1652
|
+
if (withDeploy === undefined) {
|
|
1653
|
+
if (withCi) {
|
|
1654
|
+
withDeploy = true;
|
|
1655
|
+
} else {
|
|
1656
|
+
withDeploy = rl ? await askYesNo(rl, "Add a starter deployment workflow?", false) : false;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1416
1660
|
rl?.close();
|
|
1417
1661
|
|
|
1418
1662
|
if (interactive) {
|
|
@@ -1443,9 +1687,18 @@ async function main() {
|
|
|
1443
1687
|
}
|
|
1444
1688
|
}
|
|
1445
1689
|
|
|
1446
|
-
if (withCi) {
|
|
1447
|
-
await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner
|
|
1448
|
-
|
|
1690
|
+
if (withCi || withDeploy) {
|
|
1691
|
+
await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner, {
|
|
1692
|
+
includeSecurityBundle: withCi,
|
|
1693
|
+
includeDeployWorkflow: withDeploy,
|
|
1694
|
+
});
|
|
1695
|
+
if (withCi && withDeploy) {
|
|
1696
|
+
logStep("GitHub automation added", `${skipPackageManager ? "deno" : packageManager} + deploy`);
|
|
1697
|
+
} else if (withCi) {
|
|
1698
|
+
logStep("GitHub security bundle added", skipPackageManager ? "deno" : packageManager);
|
|
1699
|
+
} else if (withDeploy) {
|
|
1700
|
+
logStep("Deploy starter added", template);
|
|
1701
|
+
}
|
|
1449
1702
|
}
|
|
1450
1703
|
|
|
1451
1704
|
if (initGit) {
|
|
@@ -1474,7 +1727,7 @@ async function main() {
|
|
|
1474
1727
|
}
|
|
1475
1728
|
}
|
|
1476
1729
|
|
|
1477
|
-
printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi });
|
|
1730
|
+
printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy });
|
|
1478
1731
|
} catch (err) {
|
|
1479
1732
|
rl?.close();
|
|
1480
1733
|
if (err && err.message === "Cancelled") {
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:9fb86079-cf96-5a72-94a8-dd7d3cdc62cc",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-
|
|
7
|
+
"timestamp": "2026-05-23T13:34:45.760Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "0.34.
|
|
12
|
+
"version": "0.34.3"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "library",
|
|
18
|
-
"bom-ref": "pkg:npm/create-daloy@0.34.
|
|
18
|
+
"bom-ref": "pkg:npm/create-daloy@0.34.3",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "0.34.
|
|
20
|
+
"version": "0.34.3",
|
|
21
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.
|
|
22
|
+
"purl": "pkg:npm/create-daloy@0.34.3",
|
|
23
23
|
"licenses": [
|
|
24
24
|
{
|
|
25
25
|
"license": {
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
40
|
"swid": {
|
|
41
|
-
"tagId": "swidtag-create-daloy-0.34.
|
|
41
|
+
"tagId": "swidtag-create-daloy-0.34.3",
|
|
42
42
|
"name": "create-daloy",
|
|
43
|
-
"version": "0.34.
|
|
43
|
+
"version": "0.34.3",
|
|
44
44
|
"tagVersion": 0,
|
|
45
45
|
"patch": false
|
|
46
46
|
}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"components": [],
|
|
50
50
|
"dependencies": [
|
|
51
51
|
{
|
|
52
|
-
"ref": "pkg:npm/create-daloy@0.34.
|
|
52
|
+
"ref": "pkg:npm/create-daloy@0.34.3",
|
|
53
53
|
"dependsOn": []
|
|
54
54
|
}
|
|
55
55
|
]
|
package/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "create-daloy-0.34.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.34.
|
|
5
|
+
"name": "create-daloy-0.34.3",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.34.3-9fb86079-cf96-5a72-94a8-dd7d3cdc62cc",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-05-
|
|
8
|
+
"created": "2026-05-23T13:34:45.760Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package-create-daloy",
|
|
18
18
|
"name": "create-daloy",
|
|
19
|
-
"versionInfo": "0.34.
|
|
19
|
+
"versionInfo": "0.34.3",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/create-daloy@0.34.
|
|
30
|
+
"referenceLocator": "pkg:npm/create-daloy@0.34.3"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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 environment protection. Once you are ready,
|
|
5
|
+
# add your preferred push/tag trigger.
|
|
6
|
+
#
|
|
7
|
+
# Default target: GitHub Container Registry (GHCR).
|
|
8
|
+
# No extra secret is required; the workflow uses the repo-scoped GITHUB_TOKEN.
|
|
9
|
+
# After the image is published, point your platform at GHCR or replace the
|
|
10
|
+
# final push steps with your provider's deploy command.
|
|
11
|
+
#
|
|
12
|
+
# Optional container-host handoff examples:
|
|
13
|
+
# - Fly.io: install flyctl in a later step and run
|
|
14
|
+
# flyctl deploy --image "$IMAGE_REPO:sha-${GITHUB_SHA}" --remote-only
|
|
15
|
+
# - Render: create an image-backed service that tracks
|
|
16
|
+
# ghcr.io/<owner>/<repo>:sha-${GITHUB_SHA}
|
|
17
|
+
|
|
18
|
+
name: Deploy
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
|
|
23
|
+
permissions: {}
|
|
24
|
+
|
|
25
|
+
concurrency:
|
|
26
|
+
group: deploy-${{ github.workflow }}-${{ github.ref }}
|
|
27
|
+
cancel-in-progress: false
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
verify:
|
|
31
|
+
name: Verify before deploy
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 20
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- name: Harden runner
|
|
39
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
40
|
+
with:
|
|
41
|
+
egress-policy: audit
|
|
42
|
+
disable-sudo: true
|
|
43
|
+
|
|
44
|
+
- name: Checkout
|
|
45
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
46
|
+
with:
|
|
47
|
+
persist-credentials: false
|
|
48
|
+
show-progress: false
|
|
49
|
+
|
|
50
|
+
- name: Set up Deno
|
|
51
|
+
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
|
52
|
+
with:
|
|
53
|
+
deno-version: v2.x
|
|
54
|
+
|
|
55
|
+
- name: Typecheck
|
|
56
|
+
run: deno task typecheck
|
|
57
|
+
|
|
58
|
+
- name: Test
|
|
59
|
+
run: deno task test
|
|
60
|
+
|
|
61
|
+
deploy:
|
|
62
|
+
name: Publish container image
|
|
63
|
+
needs: verify
|
|
64
|
+
# Lock production deploys to main or a tag push. `workflow_dispatch`
|
|
65
|
+
# accepts any branch, so this guard stops an accidental dispatch from a
|
|
66
|
+
# feature branch from shipping unreviewed code.
|
|
67
|
+
if: github.ref == 'refs/heads/main' || github.ref_type == 'tag'
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
timeout-minutes: 20
|
|
70
|
+
environment:
|
|
71
|
+
name: production
|
|
72
|
+
permissions:
|
|
73
|
+
contents: read
|
|
74
|
+
packages: write
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- name: Harden runner
|
|
78
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
79
|
+
with:
|
|
80
|
+
egress-policy: audit
|
|
81
|
+
disable-sudo: true
|
|
82
|
+
|
|
83
|
+
- name: Checkout
|
|
84
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
85
|
+
with:
|
|
86
|
+
persist-credentials: false
|
|
87
|
+
show-progress: false
|
|
88
|
+
|
|
89
|
+
- name: Log in to GHCR
|
|
90
|
+
env:
|
|
91
|
+
GH_TOKEN: ${{ github.token }}
|
|
92
|
+
run: |
|
|
93
|
+
set -eu
|
|
94
|
+
echo "$GH_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
|
95
|
+
|
|
96
|
+
- name: Derive image name
|
|
97
|
+
run: |
|
|
98
|
+
set -eu
|
|
99
|
+
owner="${GITHUB_REPOSITORY_OWNER,,}"
|
|
100
|
+
repo="${GITHUB_REPOSITORY#*/}"
|
|
101
|
+
repo="${repo,,}"
|
|
102
|
+
echo "IMAGE_REPO=ghcr.io/${owner}/${repo}" >> "$GITHUB_ENV"
|
|
103
|
+
|
|
104
|
+
- name: Build image
|
|
105
|
+
env:
|
|
106
|
+
DOCKER_BUILDKIT: "1"
|
|
107
|
+
run: |
|
|
108
|
+
set -eu
|
|
109
|
+
docker build \
|
|
110
|
+
--tag "$IMAGE_REPO:sha-${GITHUB_SHA}" \
|
|
111
|
+
--tag "$IMAGE_REPO:latest" \
|
|
112
|
+
.
|
|
113
|
+
|
|
114
|
+
- name: Push image
|
|
115
|
+
run: |
|
|
116
|
+
set -eu
|
|
117
|
+
docker push "$IMAGE_REPO:sha-${GITHUB_SHA}"
|
|
118
|
+
docker push "$IMAGE_REPO:latest"
|
|
@@ -24,6 +24,7 @@ The `--with-ci` bundle adds these defaults:
|
|
|
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
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
|
+
- A weekly scheduled `dast.yml` boots the built application and runs an OWASP ZAP baseline scan against it. CodeQL + 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.
|
|
27
28
|
- 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
|
|
|
29
30
|
## Container hardening
|
|
@@ -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__
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"gen:openapi": "deno run --allow-net --allow-env --allow-read --allow-write scripts/dump-openapi.ts"
|
|
9
9
|
},
|
|
10
10
|
"imports": {
|
|
11
|
-
"@daloyjs/core": "npm:@daloyjs/core@^0.34.
|
|
12
|
-
"@daloyjs/core/": "npm:@daloyjs/core@^0.34.
|
|
11
|
+
"@daloyjs/core": "npm:@daloyjs/core@^0.34.3",
|
|
12
|
+
"@daloyjs/core/": "npm:@daloyjs/core@^0.34.3/",
|
|
13
13
|
"zod": "npm:zod@^4.4.3"
|
|
14
14
|
},
|
|
15
15
|
"compilerOptions": {
|