javi-forge 1.25.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * L1 secret-scan section (hook-consolidation S4).
3
+ *
4
+ * Port of `templates/security-hooks/pre-commit-secrets` into TypeScript. The
5
+ * bash body built the staged-file list with `git diff --cached --name-only`
6
+ * and then piped it through `xargs git diff` (pre-commit-secrets:52) — a
7
+ * whitespace split that silently dropped any path containing a space and
8
+ * swallowed the git error with `|| true` (K-005). This port builds the staged
9
+ * list NUL-safe (`-z` → split on `"\0"`) and passes each filename as a SEPARATE
10
+ * argv element to `git diff` (no shell, no xargs), so a path like
11
+ * `app secrets.env` is scanned as ONE path and can never be split.
12
+ *
13
+ * A matched pattern in an ADDED diff line blocks the commit (ok:false).
14
+ */
15
+ import { execFileAsync } from "../../../lib/exec.js";
16
+ /**
17
+ * Ported from `templates/security-hooks/pre-commit-secrets` SECRET_PATTERNS.
18
+ * bash `(?i)` → the JS `/i` flag; bash `\x27` (a single quote) → a literal `'`.
19
+ * No `/g` flag is used, so `.test()` never carries `lastIndex` state between
20
+ * lines. Order is preserved from the bash array.
21
+ */
22
+ const SECRET_PATTERNS = [
23
+ // AWS access key id
24
+ { name: "aws-access-key", re: /AKIA[0-9A-Z]{16}/ },
25
+ // Generic API keys / tokens assigned to key-like vars
26
+ {
27
+ name: "generic-api-key",
28
+ re: /(api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token)\s*[:=]\s*["'][A-Za-z0-9+/=_-]{20,}["']/i,
29
+ },
30
+ // Private key headers
31
+ {
32
+ name: "private-key",
33
+ re: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
34
+ },
35
+ // GitHub tokens
36
+ { name: "github-token", re: /gh[pousr]_[A-Za-z0-9_]{36,}/ },
37
+ // Generic password assignments
38
+ {
39
+ name: "password-assignment",
40
+ re: /(password|passwd|pwd)\s*[:=]\s*["'][^\s"']{8,}["']/i,
41
+ },
42
+ // Slack tokens
43
+ { name: "slack-token", re: /xox[baprs]-[0-9a-zA-Z-]+/ },
44
+ // Stripe keys
45
+ { name: "stripe-secret-key", re: /sk_live_[0-9a-zA-Z]{24,}/ },
46
+ { name: "stripe-restricted-key", re: /rk_live_[0-9a-zA-Z]{24,}/ },
47
+ // SendGrid
48
+ {
49
+ name: "sendgrid-key",
50
+ re: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/,
51
+ },
52
+ ];
53
+ /** ARG_MAX guard: cap the number of file paths per `git diff` argv batch. */
54
+ export const DEFAULT_CHUNK_SIZE = 512;
55
+ /**
56
+ * maxBuffer for the content `git diff` read. Node's default is 1 MiB; a staged
57
+ * diff larger than that rejects and (caught) blocks every large legitimate
58
+ * commit with a cryptic buffer error. Raise the ceiling to a generous but
59
+ * BOUNDED 64 MiB so ordinary large commits scan, while a genuinely pathological
60
+ * diff still fails closed instead of exhausting memory.
61
+ */
62
+ export const DIFF_MAX_BUFFER = 64 * 1024 * 1024;
63
+ function defaultDeps() {
64
+ return {
65
+ execFile: async (cmd, args, opts) => {
66
+ const { stdout, stderr } = await execFileAsync(cmd, args, opts);
67
+ return { stdout: String(stdout), stderr: String(stderr) };
68
+ },
69
+ log: (m) => console.log(m),
70
+ chunkSize: DEFAULT_CHUNK_SIZE,
71
+ };
72
+ }
73
+ /** NUL-safe staged-file list (`-z` → split on "\0", drop the trailing empty). */
74
+ async function stagedFiles(deps, projectDir) {
75
+ const { stdout } = await deps.execFile("git", ["diff", "--cached", "--name-only", "--diff-filter=ACM", "-z"], { cwd: projectDir });
76
+ return stdout.split("\0").filter((f) => f.length > 0);
77
+ }
78
+ function chunk(items, size) {
79
+ const out = [];
80
+ for (let i = 0; i < items.length; i += size)
81
+ out.push(items.slice(i, i + size));
82
+ return out;
83
+ }
84
+ /**
85
+ * Walk a unified diff and report every ADDED line (`+`, never `+++`) that
86
+ * matches a secret pattern, tracking the new-file path and line number so a
87
+ * finding reads `path:line pattern`.
88
+ */
89
+ export function scanDiff(diff) {
90
+ const findings = [];
91
+ let currentFile = "";
92
+ let newLine = 0;
93
+ for (const line of diff.split("\n")) {
94
+ if (line.startsWith("+++ ")) {
95
+ const p = line.slice(4).trim();
96
+ currentFile = p === "/dev/null" ? "" : p.replace(/^b\//, "");
97
+ continue;
98
+ }
99
+ if (line.startsWith("--- "))
100
+ continue;
101
+ const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
102
+ if (hunk) {
103
+ newLine = Number.parseInt(hunk[1], 10);
104
+ continue;
105
+ }
106
+ if (line.startsWith("diff --git") ||
107
+ line.startsWith("index ") ||
108
+ line.startsWith("old mode") ||
109
+ line.startsWith("new mode") ||
110
+ line.startsWith("similarity ") ||
111
+ line.startsWith("rename ") ||
112
+ line.startsWith("new file") ||
113
+ line.startsWith("deleted file")) {
114
+ continue;
115
+ }
116
+ if (line.startsWith("+")) {
117
+ const content = line.slice(1);
118
+ for (const { name, re } of SECRET_PATTERNS) {
119
+ if (re.test(content)) {
120
+ findings.push({ file: currentFile, line: newLine, pattern: name });
121
+ }
122
+ }
123
+ newLine++;
124
+ }
125
+ else if (line.startsWith("-")) {
126
+ // removed line — does not advance the new-file line counter
127
+ }
128
+ else {
129
+ // context (leading space) or blank line
130
+ newLine++;
131
+ }
132
+ }
133
+ return findings;
134
+ }
135
+ /**
136
+ * The `secrets` section factory. Injectable seams default to the real
137
+ * `git`-via-execFileAsync implementation; tests pass mocks. A thrown git error
138
+ * becomes a blocking `{ ok: false }` (never an unhandled rejection), matching
139
+ * the ciSection/tddSection hardening.
140
+ */
141
+ export function secretsSection(overrides = {}) {
142
+ const deps = { ...defaultDeps(), ...overrides };
143
+ return {
144
+ id: "secrets",
145
+ blocking: true,
146
+ async run({ projectDir }) {
147
+ try {
148
+ const files = await stagedFiles(deps, projectDir);
149
+ if (files.length === 0)
150
+ return { ok: true };
151
+ const findings = [];
152
+ for (const batch of chunk(files, deps.chunkSize)) {
153
+ // `--no-color` is MANDATORY: under `color.ui=always` (or
154
+ // `color.diff=always`) git emits ANSI escapes even to a pipe, so
155
+ // added lines render as `\x1b[32m+secret\x1b[m` and both the
156
+ // `+`/`+++ ` line checks in scanDiff fail — the scanner would then
157
+ // find zero secrets and fail OPEN. Forcing color off keeps the diff
158
+ // plain-text and the scan deterministic.
159
+ const { stdout } = await deps.execFile("git", ["diff", "--no-color", "--cached", "--", ...batch], { cwd: projectDir, maxBuffer: DIFF_MAX_BUFFER });
160
+ findings.push(...scanDiff(stdout));
161
+ }
162
+ if (findings.length === 0)
163
+ return { ok: true };
164
+ const summary = findings
165
+ .slice(0, 10)
166
+ .map((f) => `${f.file || "<staged>"}:${f.line} ${f.pattern}`)
167
+ .join("; ");
168
+ return {
169
+ ok: false,
170
+ detail: `${findings.length} potential secret(s): ${summary}`,
171
+ };
172
+ }
173
+ catch (e) {
174
+ return {
175
+ ok: false,
176
+ detail: e instanceof Error ? e.message : String(e),
177
+ };
178
+ }
179
+ },
180
+ };
181
+ }
182
+ //# sourceMappingURL=secrets.js.map
@@ -10,10 +10,10 @@
10
10
  * behavior); a config that FAILS to validate exits 1 — a broken config never
11
11
  * silently skips a gate.
12
12
  *
13
- * S1a/S3 scope: the `ci` and `tdd` sections have bodies. The
14
- * secrets/permissions/deps sections have no factory yet (they land in S4); an
15
- * enabled-but-unregistered feature is skipped by `composeSections` until its
16
- * slice wires a factory.
13
+ * S1a/S3/S4 scope: the `ci`, `tdd`, `secrets`, `permissions` and `deps`
14
+ * sections all have factories now. `composeSections` still skips any enabled
15
+ * feature with no registered factory, so a future feature can land the same
16
+ * way.
17
17
  */
18
18
  import { type CIHooksConfig } from "../lib/ci-config.js";
19
19
  import type { Stack } from "../types/index.js";
@@ -10,14 +10,17 @@
10
10
  * behavior); a config that FAILS to validate exits 1 — a broken config never
11
11
  * silently skips a gate.
12
12
  *
13
- * S1a/S3 scope: the `ci` and `tdd` sections have bodies. The
14
- * secrets/permissions/deps sections have no factory yet (they land in S4); an
15
- * enabled-but-unregistered feature is skipped by `composeSections` until its
16
- * slice wires a factory.
13
+ * S1a/S3/S4 scope: the `ci`, `tdd`, `secrets`, `permissions` and `deps`
14
+ * sections all have factories now. `composeSections` still skips any enabled
15
+ * feature with no registered factory, so a future feature can land the same
16
+ * way.
17
17
  */
18
18
  import { spawn } from "node:child_process";
19
19
  import { findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
20
20
  import { detectCIStack, runCI } from "./ci.js";
21
+ import { depsSection } from "./hooks/sections/deps.js";
22
+ import { permissionsSection } from "./hooks/sections/permissions.js";
23
+ import { secretsSection } from "./hooks/sections/secrets.js";
21
24
  import { getTddTestCommand } from "./tdd.js";
22
25
  /** Fixed cheap→expensive order per hook (deterministic is a feature). */
23
26
  const PRE_COMMIT_ORDER = ["secrets", "permissions", "tdd", "ci"];
@@ -193,6 +196,12 @@ export function defaultRegistry(runCIImpl, log, tddDeps = {}) {
193
196
  runCommand: tddDeps.runCommand ?? runTestCommand,
194
197
  log: tddDeps.log ?? log,
195
198
  }),
199
+ // S4 security sections. Each resolves its git/fs/exec seams at run time
200
+ // via the section's own defaults; unit tests inject mocks by importing
201
+ // the factory directly (see src/commands/hooks/sections/*.test.ts).
202
+ secrets: () => secretsSection({ log }),
203
+ permissions: () => permissionsSection({ log }),
204
+ deps: () => depsSection({ log }),
196
205
  };
197
206
  }
198
207
  /** Resolve the parsed `hooks:` config for a project (null → default [ci]). */
@@ -1,28 +1,18 @@
1
1
  import type { StepFn } from "../types.js";
2
2
  /**
3
- * Step 14: Scaffold security hooks.
3
+ * Step 14: Scaffold security hooks (hook-consolidation S4 fold).
4
4
  *
5
5
  * - When options.securityHooks is false, reports "skipped".
6
- * - When SECURITY_HOOKS_DIR templates are missing, reports "error".
7
- * - Otherwise copies the 6-layer git security hooks into
8
- * ci-local/hooks/security/ (skipping existing files, chmod 0755) and copies
9
- * the kiteguard-style runtime settings to .claude/settings.json when absent.
6
+ * - Otherwise:
7
+ * 1. Copies the kiteguard-style runtime settings to `.claude/settings.json`
8
+ * when absent (KEPT this is a real feature).
9
+ * 2. Merges the `hooks:` security sections for the selected reliability
10
+ * profile into `.javi-forge/ci.yaml` via `setHookFeature` (creating a
11
+ * minimal `version: 2` config when absent). The dispatcher composes these
12
+ * sections at hook-run time (see src/commands/hooks.ts).
13
+ * - The old inert `ci-local/hooks/security/` git-hook copy is GONE (those hook
14
+ * bodies were ported to TypeScript sections in S4).
10
15
  * - Errors are swallowed and reported as status:"error" — never thrown.
11
- *
12
- * Extracted VERBATIM from src/commands/init.ts (PR 5 of 6).
13
- * Grouped with stepHookProfile for cohesion — both manage ci-local/hooks/.
14
16
  */
15
17
  export declare const stepSecurityHooks: StepFn;
16
- /**
17
- * Step 14b: Write hook reliability profile.
18
- *
19
- * - When options.securityHooks is false, reports "skipped".
20
- * - Otherwise writes ci-local/hooks/profile.json with the resolved profile
21
- * (defaults to "standard" when hookProfile is undefined).
22
- * - Errors are swallowed and reported as status:"error" — never thrown.
23
- *
24
- * Extracted VERBATIM from src/commands/init.ts (PR 5 of 6).
25
- * Grouped with stepSecurityHooks for cohesion — both manage ci-local/hooks/.
26
- */
27
- export declare const stepHookProfile: StepFn;
28
18
  //# sourceMappingURL=security.d.ts.map
@@ -1,103 +1,83 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import { SECURITY_HOOKS_DIR } from "../../../constants.js";
4
+ import { setHookFeature } from "../../../lib/ci-config.js";
4
5
  import { ensureDirExists } from "../../../lib/common.js";
5
6
  import { report } from "../report.js";
6
7
  /**
7
- * Step 14: Scaffold security hooks.
8
+ * Hook-feature preset per reliability profile (hook-consolidation S4).
8
9
  *
9
- * - When options.securityHooks is false, reports "skipped".
10
- * - When SECURITY_HOOKS_DIR templates are missing, reports "error".
11
- * - Otherwise copies the 6-layer git security hooks into
12
- * ci-local/hooks/security/ (skipping existing files, chmod 0755) and copies
13
- * the kiteguard-style runtime settings to .claude/settings.json when absent.
14
- * - Errors are swallowed and reported as status:"error" — never thrown.
15
- *
16
- * Extracted VERBATIM from src/commands/init.ts (PR 5 of 6).
17
- * Grouped with stepHookProfile for cohesion — both manage ci-local/hooks/.
10
+ * The old `stepHookProfile` wrote a `ci-local/hooks/profile.json` that had ZERO
11
+ * runtime readers (design D7). The selector is repurposed: the chosen profile
12
+ * now drives WHICH `hooks:` security sections get merged into
13
+ * `.javi-forge/ci.yaml`, using the EXISTING `HookProfile` values (no "relaxed"):
14
+ * - strict → secrets + permissions + deps (every section)
15
+ * - standard secrets + deps
16
+ * - minimal → CI gate only (no security sections)
18
17
  */
19
- export const stepSecurityHooks = async (ctx) => {
20
- const { projectDir, dryRun, onStep, options } = ctx;
21
- const { securityHooks } = options;
22
- const stepId = "security-hooks";
23
- report(onStep, stepId, "Scaffold security hooks", "running");
24
- try {
25
- if (securityHooks) {
26
- if (await fs.pathExists(SECURITY_HOOKS_DIR)) {
27
- if (!dryRun) {
28
- // Copy 6-layer git security hooks into ci-local/hooks/security/
29
- const secHooksDest = path.join(projectDir, "ci-local", "hooks", "security");
30
- await ensureDirExists(secHooksDest);
31
- const hookFiles = await fs.readdir(SECURITY_HOOKS_DIR);
32
- const gitHooks = hookFiles.filter((f) => !f.endsWith(".json"));
33
- for (const hook of gitHooks) {
34
- const src = path.join(SECURITY_HOOKS_DIR, hook);
35
- const dest = path.join(secHooksDest, hook);
36
- await fs.copy(src, dest, { overwrite: false });
37
- await fs.chmod(dest, 0o755);
38
- }
39
- // Copy runtime security settings (kiteguard-style) to .claude/
40
- const settingsSrc = path.join(SECURITY_HOOKS_DIR, "claude-settings-security.json");
41
- if (await fs.pathExists(settingsSrc)) {
42
- const claudeDir = path.join(projectDir, ".claude");
43
- await ensureDirExists(claudeDir);
44
- const settingsDest = path.join(claudeDir, "settings.json");
45
- if (!(await fs.pathExists(settingsDest))) {
46
- await fs.copy(settingsSrc, settingsDest);
47
- }
48
- }
49
- }
50
- report(onStep, stepId, "Scaffold security hooks", "done", dryRun
51
- ? "dry-run: would scaffold security hooks"
52
- : "6 git layers + runtime hooks");
53
- }
54
- else {
55
- report(onStep, stepId, "Scaffold security hooks", "error", "security-hooks templates not found");
56
- }
57
- }
58
- else {
59
- report(onStep, stepId, "Scaffold security hooks", "skipped", "not selected");
60
- }
61
- }
62
- catch (e) {
63
- report(onStep, stepId, "Scaffold security hooks", "error", String(e));
64
- }
18
+ const PROFILE_PRESET = {
19
+ minimal: { preCommit: [], prePush: [] },
20
+ standard: { preCommit: ["secrets"], prePush: ["deps"] },
21
+ strict: { preCommit: ["secrets", "permissions"], prePush: ["deps"] },
65
22
  };
66
23
  /**
67
- * Step 14b: Write hook reliability profile.
24
+ * Step 14: Scaffold security hooks (hook-consolidation S4 fold).
68
25
  *
69
26
  * - When options.securityHooks is false, reports "skipped".
70
- * - Otherwise writes ci-local/hooks/profile.json with the resolved profile
71
- * (defaults to "standard" when hookProfile is undefined).
27
+ * - Otherwise:
28
+ * 1. Copies the kiteguard-style runtime settings to `.claude/settings.json`
29
+ * when absent (KEPT — this is a real feature).
30
+ * 2. Merges the `hooks:` security sections for the selected reliability
31
+ * profile into `.javi-forge/ci.yaml` via `setHookFeature` (creating a
32
+ * minimal `version: 2` config when absent). The dispatcher composes these
33
+ * sections at hook-run time (see src/commands/hooks.ts).
34
+ * - The old inert `ci-local/hooks/security/` git-hook copy is GONE (those hook
35
+ * bodies were ported to TypeScript sections in S4).
72
36
  * - Errors are swallowed and reported as status:"error" — never thrown.
73
- *
74
- * Extracted VERBATIM from src/commands/init.ts (PR 5 of 6).
75
- * Grouped with stepSecurityHooks for cohesion — both manage ci-local/hooks/.
76
37
  */
77
- export const stepHookProfile = async (ctx) => {
38
+ export const stepSecurityHooks = async (ctx) => {
78
39
  const { projectDir, dryRun, onStep, options } = ctx;
79
40
  const { securityHooks, hookProfile } = options;
80
- const stepId = "hook-profile";
81
- report(onStep, stepId, "Write hook reliability profile", "running");
41
+ const stepId = "security-hooks";
42
+ report(onStep, stepId, "Scaffold security hooks", "running");
82
43
  try {
83
- if (securityHooks) {
84
- if (!dryRun) {
85
- const hooksDir = path.join(projectDir, "ci-local", "hooks");
86
- await ensureDirExists(hooksDir);
87
- const profilePath = path.join(hooksDir, "profile.json");
88
- const resolvedProfile = hookProfile ?? "standard";
89
- await fs.writeJson(profilePath, { profile: resolvedProfile }, { spaces: 2 });
44
+ if (!securityHooks) {
45
+ report(onStep, stepId, "Scaffold security hooks", "skipped", "not selected");
46
+ return;
47
+ }
48
+ const profile = hookProfile ?? "standard";
49
+ const preset = PROFILE_PRESET[profile];
50
+ if (dryRun) {
51
+ report(onStep, stepId, "Scaffold security hooks", "done", `dry-run: would merge ${profile} hooks preset + copy .claude/settings.json`);
52
+ return;
53
+ }
54
+ // 1. Copy the kiteguard-style runtime security settings to .claude/.
55
+ const settingsSrc = path.join(SECURITY_HOOKS_DIR, "claude-settings-security.json");
56
+ if (await fs.pathExists(settingsSrc)) {
57
+ const claudeDir = path.join(projectDir, ".claude");
58
+ await ensureDirExists(claudeDir);
59
+ const settingsDest = path.join(claudeDir, "settings.json");
60
+ if (!(await fs.pathExists(settingsDest))) {
61
+ await fs.copy(settingsSrc, settingsDest);
90
62
  }
91
- report(onStep, stepId, "Write hook reliability profile", "done", dryRun
92
- ? `dry-run: would write profile.json (${hookProfile ?? "standard"})`
93
- : `ci-local/hooks/profile.json (${hookProfile ?? "standard"})`);
94
63
  }
95
- else {
96
- report(onStep, stepId, "Write hook reliability profile", "skipped", "security hooks not selected");
64
+ // 2. Merge the profile's security sections into .javi-forge/ci.yaml.
65
+ for (const feature of preset.preCommit) {
66
+ await setHookFeature(projectDir, "pre-commit", feature, true);
67
+ }
68
+ for (const feature of preset.prePush) {
69
+ await setHookFeature(projectDir, "pre-push", feature, true);
97
70
  }
71
+ const merged = [
72
+ ...preset.preCommit.map((f) => `pre-commit.${f}`),
73
+ ...preset.prePush.map((f) => `pre-push.${f}`),
74
+ ];
75
+ report(onStep, stepId, "Scaffold security hooks", "done", merged.length > 0
76
+ ? `${profile} preset: ${merged.join(", ")}`
77
+ : `${profile} preset: CI gate only (no security sections)`);
98
78
  }
99
79
  catch (e) {
100
- report(onStep, stepId, "Write hook reliability profile", "error", String(e));
80
+ report(onStep, stepId, "Scaffold security hooks", "error", String(e));
101
81
  }
102
82
  };
103
83
  //# sourceMappingURL=security.js.map
@@ -14,7 +14,7 @@ import { stepManifest } from "./init/steps/manifest.js";
14
14
  import { stepMemory } from "./init/steps/memory.js";
15
15
  import { stepMock } from "./init/steps/mock.js";
16
16
  import { stepSDD } from "./init/steps/sdd.js";
17
- import { stepHookProfile, stepSecurityHooks } from "./init/steps/security.js";
17
+ import { stepSecurityHooks } from "./init/steps/security.js";
18
18
  /**
19
19
  * Main init orchestrator: bootstraps a project with CI, git hooks,
20
20
  * memory module, AI config sync, SDD, ghagga, and friends.
@@ -43,7 +43,6 @@ export async function initProject(options, onStep) {
43
43
  await stepClaudeMd(ctx);
44
44
  await stepDockerDeploy(ctx);
45
45
  await stepSecurityHooks(ctx);
46
- await stepHookProfile(ctx);
47
46
  await stepCodeGraph(ctx);
48
47
  await stepLocalAi(ctx);
49
48
  await stepAgentSkills(ctx);
@@ -37,7 +37,12 @@ export declare const STACK_CONTEXT_MAP: Record<string, StackContextEntry>;
37
37
  export declare const DEPLOY_TEMPLATE_MAP: Record<string, string>;
38
38
  /** Deploy destination path mapping (per CI provider) */
39
39
  export declare const DEPLOY_DESTINATION_MAP: Record<string, string>;
40
- /** Hook reliability profile definitions */
40
+ /**
41
+ * Hook security-preset definitions (hook-consolidation S4). The selected
42
+ * profile drives WHICH `hooks:` security sections get merged into
43
+ * `.javi-forge/ci.yaml` (see src/commands/init/steps/security.ts PROFILE_PRESET).
44
+ * `hooks` lists the enabled dispatcher sections for display only.
45
+ */
41
46
  export declare const HOOK_PROFILES: Record<HookProfile, {
42
47
  label: string;
43
48
  description: string;
package/dist/constants.js CHANGED
@@ -186,22 +186,27 @@ export const DEPLOY_DESTINATION_MAP = {
186
186
  gitlab: ".gitlab-ci-deploy.yml",
187
187
  woodpecker: ".woodpecker/deploy.yml",
188
188
  };
189
- /** Hook reliability profile definitions */
189
+ /**
190
+ * Hook security-preset definitions (hook-consolidation S4). The selected
191
+ * profile drives WHICH `hooks:` security sections get merged into
192
+ * `.javi-forge/ci.yaml` (see src/commands/init/steps/security.ts PROFILE_PRESET).
193
+ * `hooks` lists the enabled dispatcher sections for display only.
194
+ */
190
195
  export const HOOK_PROFILES = {
191
196
  minimal: {
192
197
  label: "Minimal",
193
- description: "pre-commit only: lint + format check",
194
- hooks: ["pre-commit"],
198
+ description: "CI gate only no security scans",
199
+ hooks: ["ci"],
195
200
  },
196
201
  standard: {
197
202
  label: "Standard",
198
- description: "pre-commit + pre-push + CI gate check",
199
- hooks: ["pre-commit", "pre-push", "ci-gate"],
203
+ description: "secret scan + dependency audit",
204
+ hooks: ["secrets", "deps", "ci"],
200
205
  },
201
206
  strict: {
202
207
  label: "Strict",
203
- description: "all standard + commit-msg validation + security scan on every push",
204
- hooks: ["pre-commit", "pre-push", "ci-gate", "commit-msg", "security-scan"],
208
+ description: "secret scan + permission checks + dependency audit",
209
+ hooks: ["secrets", "permissions", "deps", "ci"],
205
210
  },
206
211
  };
207
212
  /** Stack-to-CI template filename mapping */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.25.1",
3
+ "version": "1.26.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,29 +0,0 @@
1
- #!/bin/bash
2
- # =============================================================================
3
- # SECURITY LAYER 6: Commit Message Signing Reminder (commit-msg)
4
- # =============================================================================
5
- # Reminds developers to sign commits if signing is configured but the
6
- # current commit is not signed. Non-blocking — just a nudge.
7
- #
8
- # This complements layer 4 (pre-push signing) with an earlier reminder.
9
- # =============================================================================
10
-
11
- set -e
12
-
13
- YELLOW='\033[1;33m'
14
- CYAN='\033[0;36m'
15
- GREEN='\033[0;32m'
16
- NC='\033[0m'
17
-
18
- echo "SECURITY [6/6]: Commit signing reminder..."
19
-
20
- GPG_SIGN=$(git config --get commit.gpgsign 2>/dev/null || echo "false")
21
-
22
- if [ "$GPG_SIGN" = "true" ]; then
23
- echo -e "${GREEN} Commit signing is enabled. Good.${NC}"
24
- else
25
- echo -e "${YELLOW} Tip: Enable commit signing for supply-chain security.${NC}"
26
- echo -e "${CYAN} git config commit.gpgsign true${NC}"
27
- fi
28
-
29
- exit 0
@@ -1,74 +0,0 @@
1
- #!/bin/bash
2
- # =============================================================================
3
- # SECURITY LAYER 3: Permission Boundaries (pre-commit)
4
- # =============================================================================
5
- # Validates that staged files don't introduce overly permissive file modes,
6
- # executable flags on non-script files, or world-writable permissions.
7
- #
8
- # To skip: git commit --no-verify (NOT recommended)
9
- # =============================================================================
10
-
11
- set -e
12
-
13
- RED='\033[0;31m'
14
- YELLOW='\033[1;33m'
15
- GREEN='\033[0;32m'
16
- NC='\033[0m'
17
-
18
- echo "SECURITY [3/6]: Permission boundary check..."
19
-
20
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
21
-
22
- if [ -z "$STAGED_FILES" ]; then
23
- echo -e "${GREEN} No staged files to check.${NC}"
24
- exit 0
25
- fi
26
-
27
- ISSUES=0
28
-
29
- # Check for world-writable files (o+w)
30
- for file in $STAGED_FILES; do
31
- if [ -f "$file" ]; then
32
- PERMS=$(stat -c '%a' "$file" 2>/dev/null || stat -f '%Lp' "$file" 2>/dev/null || echo "")
33
- if [ -n "$PERMS" ]; then
34
- # Check if last digit (other permissions) has write (2, 3, 6, 7)
35
- OTHERS=${PERMS: -1}
36
- if [[ "$OTHERS" =~ [2367] ]]; then
37
- echo -e "${RED} World-writable: $file (mode $PERMS)${NC}"
38
- ISSUES=$((ISSUES + 1))
39
- fi
40
- fi
41
- fi
42
- done
43
-
44
- # Check for executable flag on non-script files
45
- SCRIPT_EXTENSIONS='\.sh$|\.bash$|\.zsh$|\.py$|\.rb$|\.pl$'
46
- for file in $STAGED_FILES; do
47
- if [ -f "$file" ] && [ -x "$file" ]; then
48
- # Allow scripts and hooks (no extension = likely a hook)
49
- BASENAME=$(basename "$file")
50
- if echo "$file" | grep -qE "$SCRIPT_EXTENSIONS"; then
51
- continue
52
- fi
53
- # Allow files in hooks/ directories
54
- if echo "$file" | grep -q '/hooks/'; then
55
- continue
56
- fi
57
- # Allow if it has a shebang
58
- if head -1 "$file" 2>/dev/null | grep -q '^#!'; then
59
- continue
60
- fi
61
- echo -e "${YELLOW} Unexpected executable: $file${NC}"
62
- ISSUES=$((ISSUES + 1))
63
- fi
64
- done
65
-
66
- if [ "$ISSUES" -gt 0 ]; then
67
- echo -e ""
68
- echo -e "${RED}COMMIT BLOCKED: $ISSUES permission issue(s) found.${NC}"
69
- echo -e "${YELLOW} Fix file permissions before committing.${NC}"
70
- exit 1
71
- fi
72
-
73
- echo -e "${GREEN} Permissions OK.${NC}"
74
- exit 0