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.
Files changed (34) hide show
  1. package/dist/cli/dispatch/tdd.d.ts +6 -3
  2. package/dist/cli/dispatch/tdd.js +62 -31
  3. package/dist/cli/help.d.ts +1 -1
  4. package/dist/cli/help.js +2 -2
  5. package/dist/commands/ci.js +14 -4
  6. package/dist/commands/doctor.d.ts +8 -0
  7. package/dist/commands/doctor.js +112 -0
  8. package/dist/commands/hooks/sections/deps.d.ts +28 -0
  9. package/dist/commands/hooks/sections/deps.js +126 -0
  10. package/dist/commands/hooks/sections/permissions.d.ts +33 -0
  11. package/dist/commands/hooks/sections/permissions.js +101 -0
  12. package/dist/commands/hooks/sections/secrets.d.ts +58 -0
  13. package/dist/commands/hooks/sections/secrets.js +182 -0
  14. package/dist/commands/hooks.d.ts +39 -3
  15. package/dist/commands/hooks.js +93 -6
  16. package/dist/commands/init/steps/security.d.ts +10 -20
  17. package/dist/commands/init/steps/security.js +58 -78
  18. package/dist/commands/init.js +1 -2
  19. package/dist/commands/tdd.d.ts +0 -14
  20. package/dist/commands/tdd.js +0 -88
  21. package/dist/constants.d.ts +6 -1
  22. package/dist/constants.js +12 -7
  23. package/dist/lib/ci-config.d.ts +10 -0
  24. package/dist/lib/ci-config.js +33 -0
  25. package/dist/types/index.d.ts +0 -7
  26. package/package.json +1 -1
  27. package/dist/commands/tdd-pipeline.d.ts +0 -17
  28. package/dist/commands/tdd-pipeline.js +0 -144
  29. package/templates/security-hooks/commit-msg-signing +0 -29
  30. package/templates/security-hooks/pre-commit-permissions +0 -74
  31. package/templates/security-hooks/pre-commit-secrets +0 -74
  32. package/templates/security-hooks/pre-push-branch-protection +0 -62
  33. package/templates/security-hooks/pre-push-deps +0 -83
  34. package/templates/security-hooks/pre-push-signing +0 -67
@@ -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
@@ -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 */
@@ -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
@@ -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
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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