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.
- package/dist/commands/ci.js +14 -4
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +112 -0
- package/dist/commands/hooks/sections/deps.d.ts +28 -0
- package/dist/commands/hooks/sections/deps.js +126 -0
- package/dist/commands/hooks/sections/permissions.d.ts +33 -0
- package/dist/commands/hooks/sections/permissions.js +101 -0
- package/dist/commands/hooks/sections/secrets.d.ts +58 -0
- package/dist/commands/hooks/sections/secrets.js +182 -0
- package/dist/commands/hooks.d.ts +4 -4
- package/dist/commands/hooks.js +13 -4
- package/dist/commands/init/steps/security.d.ts +10 -20
- package/dist/commands/init/steps/security.js +58 -78
- package/dist/commands/init.js +1 -2
- package/dist/constants.d.ts +6 -1
- package/dist/constants.js +12 -7
- package/package.json +1 -1
- package/templates/security-hooks/commit-msg-signing +0 -29
- package/templates/security-hooks/pre-commit-permissions +0 -74
- package/templates/security-hooks/pre-commit-secrets +0 -74
- package/templates/security-hooks/pre-push-branch-protection +0 -62
- package/templates/security-hooks/pre-push-deps +0 -83
- package/templates/security-hooks/pre-push-signing +0 -67
|
@@ -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
|
package/dist/commands/hooks.d.ts
CHANGED
|
@@ -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`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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";
|
package/dist/commands/hooks.js
CHANGED
|
@@ -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`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
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
|
-
*
|
|
8
|
+
* Hook-feature preset per reliability profile (hook-consolidation S4).
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
24
|
+
* Step 14: Scaffold security hooks (hook-consolidation S4 fold).
|
|
68
25
|
*
|
|
69
26
|
* - When options.securityHooks is false, reports "skipped".
|
|
70
|
-
* - Otherwise
|
|
71
|
-
*
|
|
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
|
|
38
|
+
export const stepSecurityHooks = async (ctx) => {
|
|
78
39
|
const { projectDir, dryRun, onStep, options } = ctx;
|
|
79
40
|
const { securityHooks, hookProfile } = options;
|
|
80
|
-
const stepId = "
|
|
81
|
-
report(onStep, stepId, "
|
|
41
|
+
const stepId = "security-hooks";
|
|
42
|
+
report(onStep, stepId, "Scaffold security hooks", "running");
|
|
82
43
|
try {
|
|
83
|
-
if (securityHooks) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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, "
|
|
80
|
+
report(onStep, stepId, "Scaffold security hooks", "error", String(e));
|
|
101
81
|
}
|
|
102
82
|
};
|
|
103
83
|
//# sourceMappingURL=security.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -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 {
|
|
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);
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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: "
|
|
194
|
-
hooks: ["
|
|
198
|
+
description: "CI gate only — no security scans",
|
|
199
|
+
hooks: ["ci"],
|
|
195
200
|
},
|
|
196
201
|
standard: {
|
|
197
202
|
label: "Standard",
|
|
198
|
-
description: "
|
|
199
|
-
hooks: ["
|
|
203
|
+
description: "secret scan + dependency audit",
|
|
204
|
+
hooks: ["secrets", "deps", "ci"],
|
|
200
205
|
},
|
|
201
206
|
strict: {
|
|
202
207
|
label: "Strict",
|
|
203
|
-
description: "
|
|
204
|
-
hooks: ["
|
|
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,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
|