javi-forge 1.25.0 → 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/cli/dispatch/tdd.d.ts +6 -3
- package/dist/cli/dispatch/tdd.js +62 -31
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +2 -2
- 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 +39 -3
- package/dist/commands/hooks.js +93 -6
- 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/commands/tdd.d.ts +0 -14
- package/dist/commands/tdd.js +0 -88
- package/dist/constants.d.ts +6 -1
- package/dist/constants.js +12 -7
- package/dist/lib/ci-config.d.ts +10 -0
- package/dist/lib/ci-config.js +33 -0
- package/dist/types/index.d.ts +0 -7
- package/package.json +1 -1
- package/dist/commands/tdd-pipeline.d.ts +0 -17
- package/dist/commands/tdd-pipeline.js +0 -144
- 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
package/dist/commands/tdd.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import { detectCIStack } from "./ci.js";
|
|
4
3
|
// =============================================================================
|
|
5
4
|
// Test command resolution
|
|
6
5
|
// =============================================================================
|
|
@@ -30,91 +29,4 @@ export async function getTddTestCommand(stack, buildTool, projectDir) {
|
|
|
30
29
|
return null;
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// Hook generation
|
|
35
|
-
// =============================================================================
|
|
36
|
-
/**
|
|
37
|
-
* Generate a TDD-enforcing pre-commit hook script.
|
|
38
|
-
* If testCmd is null, generates a warning-only hook.
|
|
39
|
-
*/
|
|
40
|
-
export function generateTddHook(testCmd, stack) {
|
|
41
|
-
if (!testCmd) {
|
|
42
|
-
return `#!/bin/bash
|
|
43
|
-
# =============================================================================
|
|
44
|
-
# TDD PRE-COMMIT: No test command detected for stack "${stack}"
|
|
45
|
-
# =============================================================================
|
|
46
|
-
# Install a test runner and re-run: javi-forge tdd init
|
|
47
|
-
# =============================================================================
|
|
48
|
-
|
|
49
|
-
echo "TDD HOOK: No test command configured for stack '${stack}' — skipping."
|
|
50
|
-
exit 0
|
|
51
|
-
`;
|
|
52
|
-
}
|
|
53
|
-
return `#!/bin/bash
|
|
54
|
-
# =============================================================================
|
|
55
|
-
# TDD PRE-COMMIT: Enforced test-driven development
|
|
56
|
-
# =============================================================================
|
|
57
|
-
# Flow: Tests MUST pass before commit is allowed.
|
|
58
|
-
# Stack: ${stack} | Command: ${testCmd}
|
|
59
|
-
# To skip: git commit --no-verify
|
|
60
|
-
# =============================================================================
|
|
61
|
-
|
|
62
|
-
set -e
|
|
63
|
-
|
|
64
|
-
echo "TDD PRE-COMMIT: Running tests..."
|
|
65
|
-
echo " Stack: ${stack}"
|
|
66
|
-
echo " Command: ${testCmd}"
|
|
67
|
-
echo ""
|
|
68
|
-
|
|
69
|
-
${testCmd} || {
|
|
70
|
-
echo ""
|
|
71
|
-
echo "TDD FAILED — Tests did not pass."
|
|
72
|
-
echo " Fix failing tests before committing."
|
|
73
|
-
echo " To skip: git commit --no-verify"
|
|
74
|
-
exit 1
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
echo ""
|
|
78
|
-
echo "TDD PASSED — All tests green. Commit allowed."
|
|
79
|
-
`;
|
|
80
|
-
}
|
|
81
|
-
// =============================================================================
|
|
82
|
-
// Hook installation
|
|
83
|
-
// =============================================================================
|
|
84
|
-
/**
|
|
85
|
-
* Install TDD pre-commit hook into .git/hooks/.
|
|
86
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
87
|
-
*/
|
|
88
|
-
export async function installTddHooks(projectDir) {
|
|
89
|
-
const gitDir = path.join(projectDir, ".git");
|
|
90
|
-
if (!(await fs.pathExists(gitDir))) {
|
|
91
|
-
return {
|
|
92
|
-
installed: [],
|
|
93
|
-
errors: ["Not a git repository. Run git init first."],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const hooksDir = path.join(gitDir, "hooks");
|
|
97
|
-
await fs.ensureDir(hooksDir);
|
|
98
|
-
// Detect stack
|
|
99
|
-
let stackInfo;
|
|
100
|
-
try {
|
|
101
|
-
stackInfo = await detectCIStack(projectDir);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return { installed: [], errors: ["Failed to detect project stack."] };
|
|
105
|
-
}
|
|
106
|
-
const testCmd = await getTddTestCommand(stackInfo.stackType, stackInfo.buildTool, projectDir);
|
|
107
|
-
const hookContent = generateTddHook(testCmd, stackInfo.stackType);
|
|
108
|
-
const installed = [];
|
|
109
|
-
const errors = [];
|
|
110
|
-
const hookPath = path.join(hooksDir, "pre-commit");
|
|
111
|
-
try {
|
|
112
|
-
await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
|
|
113
|
-
installed.push("pre-commit");
|
|
114
|
-
}
|
|
115
|
-
catch (e) {
|
|
116
|
-
errors.push(`pre-commit: ${e instanceof Error ? e.message : String(e)}`);
|
|
117
|
-
}
|
|
118
|
-
return { installed, errors };
|
|
119
|
-
}
|
|
120
32
|
//# sourceMappingURL=tdd.js.map
|
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/dist/lib/ci-config.d.ts
CHANGED
|
@@ -144,6 +144,16 @@ export declare function parseCIConfig(rawYaml: string, source?: string): CIConfi
|
|
|
144
144
|
* validation error throws — no config is ever silently ignored.
|
|
145
145
|
*/
|
|
146
146
|
export declare function loadCIConfig(configPath: string): Promise<CIConfig>;
|
|
147
|
+
/**
|
|
148
|
+
* Set a single `hooks.<hook>.<feature>` flag in a project's CI config, creating
|
|
149
|
+
* a minimal `version: 2` `.javi-forge/ci.yaml` when none exists. Existing
|
|
150
|
+
* content — runners, gates, other hooks, comments and formatting — is preserved
|
|
151
|
+
* via a YAML document round-trip (`parseDocument`), so this NEVER clobbers a
|
|
152
|
+
* hand-authored config. `hooks:` is a v2-only key, so the version is bumped to 2
|
|
153
|
+
* when a hook feature is written into an older document. Returns the path
|
|
154
|
+
* written. Fail-closed callers still validate on the next `loadCIConfig`.
|
|
155
|
+
*/
|
|
156
|
+
export declare function setHookFeature(projectDir: string, hook: "pre-commit" | "pre-push", feature: string, value: boolean | string): Promise<string>;
|
|
147
157
|
/**
|
|
148
158
|
* Discover the default CI config for a project directory.
|
|
149
159
|
* Returns the config path, or null when the project has no config
|
package/dist/lib/ci-config.js
CHANGED
|
@@ -633,6 +633,39 @@ export async function loadCIConfig(configPath) {
|
|
|
633
633
|
const raw = await fs.readFile(configPath, "utf-8");
|
|
634
634
|
return parseCIConfig(raw, configPath);
|
|
635
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Set a single `hooks.<hook>.<feature>` flag in a project's CI config, creating
|
|
638
|
+
* a minimal `version: 2` `.javi-forge/ci.yaml` when none exists. Existing
|
|
639
|
+
* content — runners, gates, other hooks, comments and formatting — is preserved
|
|
640
|
+
* via a YAML document round-trip (`parseDocument`), so this NEVER clobbers a
|
|
641
|
+
* hand-authored config. `hooks:` is a v2-only key, so the version is bumped to 2
|
|
642
|
+
* when a hook feature is written into an older document. Returns the path
|
|
643
|
+
* written. Fail-closed callers still validate on the next `loadCIConfig`.
|
|
644
|
+
*/
|
|
645
|
+
export async function setHookFeature(projectDir, hook, feature, value) {
|
|
646
|
+
const existing = await findCIConfig(projectDir);
|
|
647
|
+
const configPath = existing ?? path.join(projectDir, CI_CONFIG_CANDIDATES[0]);
|
|
648
|
+
const doc = existing
|
|
649
|
+
? YAML.parseDocument(await fs.readFile(existing, "utf-8"))
|
|
650
|
+
: YAML.parseDocument("version: 2\n");
|
|
651
|
+
// Fail-closed: never write over a malformed config. A parse error means the
|
|
652
|
+
// document is only partially represented, so setIn + write would silently
|
|
653
|
+
// drop the unparseable remainder of a hand-authored file.
|
|
654
|
+
if (doc.errors.length > 0) {
|
|
655
|
+
throw new CIConfigError(doc.errors.map((e) => ({
|
|
656
|
+
path: "<document>",
|
|
657
|
+
message: `invalid YAML: ${e.message.split("\n")[0]}`,
|
|
658
|
+
})), configPath);
|
|
659
|
+
}
|
|
660
|
+
// `hooks:` requires version 2; bump an older/unset document in place.
|
|
661
|
+
if (doc.get("version") !== 2) {
|
|
662
|
+
doc.set("version", 2);
|
|
663
|
+
}
|
|
664
|
+
doc.setIn(["hooks", hook, feature], value);
|
|
665
|
+
await fs.ensureDir(path.dirname(configPath));
|
|
666
|
+
await fs.writeFile(configPath, doc.toString());
|
|
667
|
+
return configPath;
|
|
668
|
+
}
|
|
636
669
|
/**
|
|
637
670
|
* Discover the default CI config for a project directory.
|
|
638
671
|
* Returns the config path, or null when the project has no config
|
package/dist/types/index.d.ts
CHANGED
|
@@ -67,13 +67,6 @@ export interface DoctorSection {
|
|
|
67
67
|
export interface DoctorResult {
|
|
68
68
|
sections: DoctorSection[];
|
|
69
69
|
}
|
|
70
|
-
export type TddPipelineMode = "strict" | "warn";
|
|
71
|
-
export interface TddPipelineResult {
|
|
72
|
-
installed: string[];
|
|
73
|
-
skipped: string[];
|
|
74
|
-
errors: string[];
|
|
75
|
-
mode: TddPipelineMode;
|
|
76
|
-
}
|
|
77
70
|
export interface PluginManifest {
|
|
78
71
|
name: string;
|
|
79
72
|
version: string;
|
package/package.json
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Stack, TddPipelineMode, TddPipelineResult } from "../types/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Generate a TDD pipeline enforcement pre-push hook script.
|
|
4
|
-
*
|
|
5
|
-
* - strict: tests MUST pass or push is blocked (exit 1).
|
|
6
|
-
* - warn: tests are run and results shown, but push is never blocked.
|
|
7
|
-
*
|
|
8
|
-
* If testCmd is null, generates a skip-only hook regardless of mode.
|
|
9
|
-
*/
|
|
10
|
-
export declare function generateTddPipelineHook(mode: TddPipelineMode, testCmd: string | null, stack: Stack): string;
|
|
11
|
-
/**
|
|
12
|
-
* Install TDD pipeline pre-push hook into .git/hooks/.
|
|
13
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
14
|
-
* Backs up any existing pre-push hook to pre-push.bak.
|
|
15
|
-
*/
|
|
16
|
-
export declare function installTddPipelineHook(projectDir: string, mode: TddPipelineMode): Promise<TddPipelineResult>;
|
|
17
|
-
//# sourceMappingURL=tdd-pipeline.d.ts.map
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "fs-extra";
|
|
3
|
-
import { detectCIStack } from "./ci.js";
|
|
4
|
-
import { getTddTestCommand } from "./tdd.js";
|
|
5
|
-
// =============================================================================
|
|
6
|
-
// Hook generation
|
|
7
|
-
// =============================================================================
|
|
8
|
-
/**
|
|
9
|
-
* Generate a TDD pipeline enforcement pre-push hook script.
|
|
10
|
-
*
|
|
11
|
-
* - strict: tests MUST pass or push is blocked (exit 1).
|
|
12
|
-
* - warn: tests are run and results shown, but push is never blocked.
|
|
13
|
-
*
|
|
14
|
-
* If testCmd is null, generates a skip-only hook regardless of mode.
|
|
15
|
-
*/
|
|
16
|
-
export function generateTddPipelineHook(mode, testCmd, stack) {
|
|
17
|
-
if (!testCmd) {
|
|
18
|
-
return `#!/bin/bash
|
|
19
|
-
# =============================================================================
|
|
20
|
-
# TDD PIPELINE (pre-push): No test command detected for stack "${stack}"
|
|
21
|
-
# =============================================================================
|
|
22
|
-
# Install a test runner and re-run: javi-forge tdd pipeline --mode ${mode}
|
|
23
|
-
# =============================================================================
|
|
24
|
-
|
|
25
|
-
echo "TDD PIPELINE: No test command configured for stack '${stack}' — skipping."
|
|
26
|
-
exit 0
|
|
27
|
-
`;
|
|
28
|
-
}
|
|
29
|
-
if (mode === "warn") {
|
|
30
|
-
return `#!/bin/bash
|
|
31
|
-
# =============================================================================
|
|
32
|
-
# TDD PIPELINE (pre-push): WARN mode
|
|
33
|
-
# =============================================================================
|
|
34
|
-
# Flow: Spec → Tests → Fail → Implement → Pass
|
|
35
|
-
# Stack: ${stack} | Command: ${testCmd}
|
|
36
|
-
# Mode: warn — tests run but push is NEVER blocked
|
|
37
|
-
# To skip: git push --no-verify
|
|
38
|
-
# =============================================================================
|
|
39
|
-
|
|
40
|
-
echo "TDD PIPELINE [WARN]: Running tests before push..."
|
|
41
|
-
echo " Stack: ${stack}"
|
|
42
|
-
echo " Command: ${testCmd}"
|
|
43
|
-
echo " Mode: warn (push will proceed regardless)"
|
|
44
|
-
echo ""
|
|
45
|
-
|
|
46
|
-
${testCmd} && {
|
|
47
|
-
echo ""
|
|
48
|
-
echo "TDD PIPELINE [WARN]: All tests passed."
|
|
49
|
-
} || {
|
|
50
|
-
echo ""
|
|
51
|
-
echo "TDD PIPELINE [WARN]: Tests FAILED — but push will proceed (warn mode)."
|
|
52
|
-
echo " Consider fixing tests before merging."
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
exit 0
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
58
|
-
// strict mode (default)
|
|
59
|
-
return `#!/bin/bash
|
|
60
|
-
# =============================================================================
|
|
61
|
-
# TDD PIPELINE (pre-push): STRICT mode
|
|
62
|
-
# =============================================================================
|
|
63
|
-
# Flow: Spec → Tests → Fail → Implement → Pass
|
|
64
|
-
# Stack: ${stack} | Command: ${testCmd}
|
|
65
|
-
# Mode: strict — push is BLOCKED if tests fail
|
|
66
|
-
# To skip: git push --no-verify
|
|
67
|
-
# =============================================================================
|
|
68
|
-
|
|
69
|
-
set -e
|
|
70
|
-
|
|
71
|
-
echo "TDD PIPELINE [STRICT]: Running tests before push..."
|
|
72
|
-
echo " Stack: ${stack}"
|
|
73
|
-
echo " Command: ${testCmd}"
|
|
74
|
-
echo " Mode: strict (push blocked on failure)"
|
|
75
|
-
echo ""
|
|
76
|
-
|
|
77
|
-
${testCmd} || {
|
|
78
|
-
echo ""
|
|
79
|
-
echo "TDD PIPELINE [STRICT]: FAILED — Tests did not pass."
|
|
80
|
-
echo " Fix failing tests before pushing."
|
|
81
|
-
echo " To skip: git push --no-verify"
|
|
82
|
-
exit 1
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
echo ""
|
|
86
|
-
echo "TDD PIPELINE [STRICT]: All tests passed. Push allowed."
|
|
87
|
-
`;
|
|
88
|
-
}
|
|
89
|
-
// =============================================================================
|
|
90
|
-
// Hook installation
|
|
91
|
-
// =============================================================================
|
|
92
|
-
/**
|
|
93
|
-
* Install TDD pipeline pre-push hook into .git/hooks/.
|
|
94
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
95
|
-
* Backs up any existing pre-push hook to pre-push.bak.
|
|
96
|
-
*/
|
|
97
|
-
export async function installTddPipelineHook(projectDir, mode) {
|
|
98
|
-
const result = {
|
|
99
|
-
installed: [],
|
|
100
|
-
skipped: [],
|
|
101
|
-
errors: [],
|
|
102
|
-
mode,
|
|
103
|
-
};
|
|
104
|
-
const gitDir = path.join(projectDir, ".git");
|
|
105
|
-
if (!(await fs.pathExists(gitDir))) {
|
|
106
|
-
result.errors.push("Not a git repository. Run git init first.");
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
const hooksDir = path.join(gitDir, "hooks");
|
|
110
|
-
await fs.ensureDir(hooksDir);
|
|
111
|
-
// Detect stack
|
|
112
|
-
let stackInfo;
|
|
113
|
-
try {
|
|
114
|
-
stackInfo = await detectCIStack(projectDir);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
result.errors.push("Failed to detect project stack.");
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
const testCmd = await getTddTestCommand(stackInfo.stackType, stackInfo.buildTool, projectDir);
|
|
121
|
-
const hookContent = generateTddPipelineHook(mode, testCmd, stackInfo.stackType);
|
|
122
|
-
const hookPath = path.join(hooksDir, "pre-push");
|
|
123
|
-
// Backup existing hook
|
|
124
|
-
if (await fs.pathExists(hookPath)) {
|
|
125
|
-
const backupPath = path.join(hooksDir, "pre-push.bak");
|
|
126
|
-
try {
|
|
127
|
-
await fs.copy(hookPath, backupPath, { overwrite: true });
|
|
128
|
-
result.skipped.push("pre-push (backed up to pre-push.bak)");
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
result.errors.push(`backup: ${e instanceof Error ? e.message : String(e)}`);
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
|
|
137
|
-
result.installed.push("pre-push");
|
|
138
|
-
}
|
|
139
|
-
catch (e) {
|
|
140
|
-
result.errors.push(`pre-push: ${e instanceof Error ? e.message : String(e)}`);
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
//# sourceMappingURL=tdd-pipeline.js.map
|
|
@@ -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
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# SECURITY LAYER 1: Secret Scanning (pre-commit)
|
|
4
|
-
# =============================================================================
|
|
5
|
-
# Scans staged files for hardcoded secrets, API keys, and credentials.
|
|
6
|
-
# Runs on every commit to prevent accidental secret leaks.
|
|
7
|
-
#
|
|
8
|
-
# Requires: git
|
|
9
|
-
# To skip: git commit --no-verify (NOT recommended)
|
|
10
|
-
# =============================================================================
|
|
11
|
-
|
|
12
|
-
set -e
|
|
13
|
-
|
|
14
|
-
RED='\033[0;31m'
|
|
15
|
-
YELLOW='\033[1;33m'
|
|
16
|
-
GREEN='\033[0;32m'
|
|
17
|
-
NC='\033[0m'
|
|
18
|
-
|
|
19
|
-
echo "SECURITY [1/6]: Secret scanning..."
|
|
20
|
-
|
|
21
|
-
# Patterns that indicate hardcoded secrets
|
|
22
|
-
SECRET_PATTERNS=(
|
|
23
|
-
# AWS
|
|
24
|
-
'AKIA[0-9A-Z]{16}'
|
|
25
|
-
# Generic API keys (long hex/base64 strings assigned to key-like vars)
|
|
26
|
-
'(?i)(api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token)\s*[:=]\s*["\x27][A-Za-z0-9+/=_-]{20,}["\x27]'
|
|
27
|
-
# Private keys
|
|
28
|
-
'-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'
|
|
29
|
-
# GitHub tokens
|
|
30
|
-
'gh[pousr]_[A-Za-z0-9_]{36,}'
|
|
31
|
-
# Generic password assignments
|
|
32
|
-
'(?i)(password|passwd|pwd)\s*[:=]\s*["\x27][^\s"'\'']{8,}["\x27]'
|
|
33
|
-
# Slack tokens
|
|
34
|
-
'xox[baprs]-[0-9a-zA-Z-]+'
|
|
35
|
-
# Stripe keys
|
|
36
|
-
'sk_live_[0-9a-zA-Z]{24,}'
|
|
37
|
-
'rk_live_[0-9a-zA-Z]{24,}'
|
|
38
|
-
# SendGrid
|
|
39
|
-
'SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}'
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
43
|
-
|
|
44
|
-
if [ -z "$STAGED_FILES" ]; then
|
|
45
|
-
echo -e "${GREEN} No staged files to scan.${NC}"
|
|
46
|
-
exit 0
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
FOUND=0
|
|
50
|
-
for pattern in "${SECRET_PATTERNS[@]}"; do
|
|
51
|
-
# Use grep -P for perl-compatible regex, fall back to -E
|
|
52
|
-
MATCHES=$(echo "$STAGED_FILES" | xargs git diff --cached -- | grep -nP "$pattern" 2>/dev/null || true)
|
|
53
|
-
if [ -n "$MATCHES" ]; then
|
|
54
|
-
if [ "$FOUND" -eq 0 ]; then
|
|
55
|
-
echo -e "${RED} Potential secrets detected in staged changes:${NC}"
|
|
56
|
-
fi
|
|
57
|
-
echo -e "${YELLOW} Pattern: $pattern${NC}"
|
|
58
|
-
echo "$MATCHES" | head -5 | while read -r line; do
|
|
59
|
-
echo -e "${RED} $line${NC}"
|
|
60
|
-
done
|
|
61
|
-
FOUND=$((FOUND + 1))
|
|
62
|
-
fi
|
|
63
|
-
done
|
|
64
|
-
|
|
65
|
-
if [ "$FOUND" -gt 0 ]; then
|
|
66
|
-
echo -e ""
|
|
67
|
-
echo -e "${RED}COMMIT BLOCKED: $FOUND secret pattern(s) detected.${NC}"
|
|
68
|
-
echo -e "${YELLOW} Remove secrets and use environment variables instead.${NC}"
|
|
69
|
-
echo -e "${YELLOW} If this is a false positive, add to .secret-scan-ignore${NC}"
|
|
70
|
-
exit 1
|
|
71
|
-
fi
|
|
72
|
-
|
|
73
|
-
echo -e "${GREEN} No secrets found.${NC}"
|
|
74
|
-
exit 0
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# SECURITY LAYER 5: Branch Protection (pre-push)
|
|
4
|
-
# =============================================================================
|
|
5
|
-
# Prevents direct pushes to protected branches (main, master, release/*).
|
|
6
|
-
# Forces use of pull requests for protected branches.
|
|
7
|
-
#
|
|
8
|
-
# To skip: git push --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 [5/6]: Branch protection check..."
|
|
19
|
-
|
|
20
|
-
# Protected branch patterns
|
|
21
|
-
PROTECTED_BRANCHES=(
|
|
22
|
-
"main"
|
|
23
|
-
"master"
|
|
24
|
-
"release/*"
|
|
25
|
-
"production"
|
|
26
|
-
"staging"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
30
|
-
|
|
31
|
-
if [ -z "$CURRENT_BRANCH" ]; then
|
|
32
|
-
echo -e "${YELLOW} Could not determine current branch. Skipping.${NC}"
|
|
33
|
-
exit 0
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
for pattern in "${PROTECTED_BRANCHES[@]}"; do
|
|
37
|
-
# Support glob patterns
|
|
38
|
-
if [[ "$CURRENT_BRANCH" == $pattern ]]; then
|
|
39
|
-
# Check if we're on CI (allow CI pushes)
|
|
40
|
-
if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ] || [ -n "$GITLAB_CI" ]; then
|
|
41
|
-
echo -e "${GREEN} CI environment detected. Allowing push to $CURRENT_BRANCH.${NC}"
|
|
42
|
-
exit 0
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
echo -e ""
|
|
46
|
-
echo -e "${RED}PUSH BLOCKED: Direct push to protected branch '$CURRENT_BRANCH'.${NC}"
|
|
47
|
-
echo -e ""
|
|
48
|
-
echo -e "${YELLOW} Protected branches require pull requests:${NC}"
|
|
49
|
-
for b in "${PROTECTED_BRANCHES[@]}"; do
|
|
50
|
-
echo -e "${YELLOW} - $b${NC}"
|
|
51
|
-
done
|
|
52
|
-
echo -e ""
|
|
53
|
-
echo -e "${YELLOW} Create a feature branch and open a PR instead:${NC}"
|
|
54
|
-
echo -e "${GREEN} git checkout -b feat/my-feature${NC}"
|
|
55
|
-
echo -e "${GREEN} git push -u origin feat/my-feature${NC}"
|
|
56
|
-
echo -e "${GREEN} gh pr create${NC}"
|
|
57
|
-
exit 1
|
|
58
|
-
fi
|
|
59
|
-
done
|
|
60
|
-
|
|
61
|
-
echo -e "${GREEN} Branch '$CURRENT_BRANCH' is not protected. OK.${NC}"
|
|
62
|
-
exit 0
|