@whitehatd/crag 0.2.2 → 0.2.4

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.
@@ -3,11 +3,21 @@
3
3
  /**
4
4
  * Escape a string for safe inclusion inside double quotes in a shell command.
5
5
  * Escapes: backslash, backtick, dollar sign, double quote.
6
+ * Backslash MUST be replaced first so its replacement isn't re-escaped.
6
7
  */
7
8
  function shellEscapeDoubleQuoted(s) {
8
9
  return String(s).replace(/[\\`"$]/g, '\\$&');
9
10
  }
10
11
 
12
+ /**
13
+ * Escape a string for safe inclusion inside single quotes in a shell command.
14
+ * Single quotes cannot be escaped inside single quotes — the standard pattern
15
+ * is to close the quote, emit an escaped quote, and reopen: 'foo'\''bar'.
16
+ */
17
+ function shellEscapeSingleQuoted(s) {
18
+ return String(s).replace(/'/g, "'\\''");
19
+ }
20
+
11
21
  /**
12
22
  * Convert human-readable gate descriptions to shell commands.
13
23
  * e.g. Verify src/skills/pre-start-context.md contains "discovers any project"
@@ -25,4 +35,4 @@ function gateToShell(cmd) {
25
35
  return cmd;
26
36
  }
27
37
 
28
- module.exports = { gateToShell, shellEscapeDoubleQuoted };
38
+ module.exports = { gateToShell, shellEscapeDoubleQuoted, shellEscapeSingleQuoted };
@@ -14,6 +14,30 @@
14
14
  // Protects against ReDoS on catastrophic-backtracking-prone regex.
15
15
  const MAX_CONTENT_SIZE = 256 * 1024; // 256 KB
16
16
 
17
+ /**
18
+ * Validate an annotation path (used for `path:` and `if:` on gate sections).
19
+ *
20
+ * Rejects:
21
+ * - Absolute paths (/, C:\, \\server\share)
22
+ * - Parent traversal (..)
23
+ * - Newlines or null bytes (defense against injection into generated YAML/shell)
24
+ *
25
+ * These paths are interpolated into shell commands and YAML scalars downstream
26
+ * (husky, pre-commit, github-actions), so the parser is the single chokepoint
27
+ * where untrusted path strings from governance.md become structured data.
28
+ */
29
+ function isValidAnnotationPath(p) {
30
+ if (typeof p !== 'string' || p.length === 0) return false;
31
+ if (p.length > 512) return false;
32
+ if (/[\n\r\x00]/.test(p)) return false;
33
+ // POSIX absolute or Windows drive-letter / UNC
34
+ if (p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('\\\\')) return false;
35
+ // Parent traversal (match as a path segment, not as substring of a name)
36
+ const segments = p.split(/[\\/]/);
37
+ if (segments.includes('..')) return false;
38
+ return true;
39
+ }
40
+
17
41
  /**
18
42
  * Extract a markdown section body by heading name.
19
43
  * Starts after the first line matching `## <name>` (with optional trailing text),
@@ -90,8 +114,22 @@ function parseGovernance(content) {
90
114
  if (sub) {
91
115
  section = sub[1].trim().toLowerCase();
92
116
  sectionMeta = { path: null, condition: null };
93
- if (sub[2] === 'path') sectionMeta.path = sub[3].trim();
94
- if (sub[2] === 'if') sectionMeta.condition = sub[3].trim();
117
+ if (sub[2] === 'path') {
118
+ const raw = sub[3].trim();
119
+ if (isValidAnnotationPath(raw)) {
120
+ sectionMeta.path = raw;
121
+ } else {
122
+ result.warnings.push(`Invalid path annotation in section "${sub[1].trim()}": ${JSON.stringify(raw)} (must be a relative in-repo path without "..")`);
123
+ }
124
+ }
125
+ if (sub[2] === 'if') {
126
+ const raw = sub[3].trim();
127
+ if (isValidAnnotationPath(raw)) {
128
+ sectionMeta.condition = raw;
129
+ } else {
130
+ result.warnings.push(`Invalid if annotation in section "${sub[1].trim()}": ${JSON.stringify(raw)} (must be a relative in-repo path without "..")`);
131
+ }
132
+ }
95
133
  result.gates[section] = {
96
134
  commands: [],
97
135
  path: sectionMeta.path,
@@ -179,4 +217,4 @@ function flattenGatesRich(gates) {
179
217
  return out;
180
218
  }
181
219
 
182
- module.exports = { parseGovernance, flattenGates, flattenGatesRich, extractSection };
220
+ module.exports = { parseGovernance, flattenGates, flattenGatesRich, extractSection, isValidAnnotationPath };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared YAML `run:` command extraction for GitHub Actions workflows.
5
+ *
6
+ * Both `crag analyze` and `crag diff` need to enumerate the shell commands
7
+ * inside CI workflows to either generate gates from them or compare them
8
+ * against governance. This module is the single source of truth so a fix to
9
+ * the parser benefits both commands.
10
+ *
11
+ * Handles:
12
+ * run: npm test (inline)
13
+ * run: "npm test" (inline, quoted)
14
+ * run: | (literal block scalar)
15
+ * npm test
16
+ * npm run build
17
+ * run: >- (folded block scalar)
18
+ * npm test
19
+ *
20
+ * Comment-only lines and blank lines inside blocks are skipped.
21
+ */
22
+ function extractRunCommands(content) {
23
+ const commands = [];
24
+ const lines = String(content).split(/\r?\n/);
25
+
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i];
28
+ const m = line.match(/^(\s*)-?\s*run:\s*(.*)$/);
29
+ if (!m) continue;
30
+
31
+ const baseIndent = m[1].length;
32
+ const rest = m[2].trim();
33
+
34
+ if (/^[|>][+-]?\s*$/.test(rest)) {
35
+ // Block scalar: collect following lines with greater indent than the key
36
+ for (let j = i + 1; j < lines.length; j++) {
37
+ const ln = lines[j];
38
+ if (ln.trim() === '') continue;
39
+ const indentMatch = ln.match(/^(\s*)/);
40
+ if (indentMatch[1].length <= baseIndent) break;
41
+ const trimmed = ln.trim();
42
+ if (trimmed && !trimmed.startsWith('#')) commands.push(trimmed);
43
+ }
44
+ } else if (rest && !rest.startsWith('#')) {
45
+ // Inline: strip surrounding single/double quotes if present
46
+ commands.push(rest.replace(/^["']|["']$/g, ''));
47
+ }
48
+ }
49
+
50
+ return commands;
51
+ }
52
+
53
+ /**
54
+ * Classify a shell command as a "gate" — i.e., a quality check that belongs
55
+ * in governance.md (test, lint, typecheck, build, etc.) as opposed to
56
+ * deployment, git operations, or environment setup.
57
+ *
58
+ * This is a heuristic and intentionally conservative: false positives
59
+ * (extra gates) are easier to spot than false negatives (missing gates).
60
+ */
61
+ function isGateCommand(cmd) {
62
+ const patterns = [
63
+ // Node ecosystem
64
+ /\bnpm (run |ci|test|install)/,
65
+ /\bnpx /,
66
+ /\bnode /,
67
+ /\byarn (test|lint|build|check)/,
68
+ /\bpnpm (run |test|lint|build|check|install|i\b)/,
69
+ /\bbun (test|run)/,
70
+ /\bdeno (test|lint|fmt|check)/,
71
+ // Rust
72
+ /\bcargo (test|build|check|clippy|fmt)/,
73
+ /\brustfmt/,
74
+ // Go
75
+ /\bgo (test|build|vet)/,
76
+ /\bgolangci-lint/,
77
+ // Python — direct + modern runner wrappers
78
+ /\bpytest/,
79
+ /\bpython -m/,
80
+ /\bruff/,
81
+ /\bmypy/,
82
+ /\bflake8/,
83
+ /\bblack\b/,
84
+ /\bisort\b/,
85
+ /\bpylint\b/,
86
+ /\btox\s+(run|r)/,
87
+ /\buv run /,
88
+ /\bpoetry run /,
89
+ /\bpdm run /,
90
+ /\bhatch run /,
91
+ /\brye run /,
92
+ /\bnox\b/,
93
+ // JVM
94
+ /\bgradle/,
95
+ /\bmvn /,
96
+ /\bmaven/,
97
+ /\.\/gradlew/,
98
+ /\.\/mvnw/,
99
+ // Ruby
100
+ /\bbundle exec /,
101
+ /\brake\b/,
102
+ /\brspec\b/,
103
+ /\brubocop/,
104
+ // PHP
105
+ /\bcomposer (test|lint|run|validate)/,
106
+ /\bvendor\/bin\/(phpunit|phpcs|phpstan|psalm|pest|php-cs-fixer|rector)/,
107
+ // .NET
108
+ /\bdotnet (test|build|format)/,
109
+ // Swift
110
+ /\bswift (test|build)/,
111
+ /\bswiftlint/,
112
+ // Elixir
113
+ /\bmix (test|format|credo|dialyzer)/,
114
+ // Node linters
115
+ /\beslint/,
116
+ /\bbiome/,
117
+ /\bprettier/,
118
+ /\btsc/,
119
+ /\bxo\b/,
120
+ // Task runners
121
+ /\bmake /,
122
+ /\bjust /,
123
+ /\btask /,
124
+ // Containers / infra
125
+ /\bdocker (build|compose)/,
126
+ /\bterraform (fmt|validate|plan)/,
127
+ /\btflint/,
128
+ /\bhelm (lint|template)/,
129
+ /\bkubeconform/,
130
+ /\bkubeval/,
131
+ /\bhadolint/,
132
+ /\bactionlint/,
133
+ /\bmarkdownlint/,
134
+ /\byamllint/,
135
+ /\bbuf (lint|build)/,
136
+ /\bspectral lint/,
137
+ /\bshellcheck/,
138
+ /\bsemgrep/,
139
+ /\btrivy/,
140
+ /\bgitleaks/,
141
+ ];
142
+ return patterns.some((p) => p.test(cmd));
143
+ }
144
+
145
+ module.exports = { extractRunCommands, isGateCommand };
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: post-start-validation
3
- version: 0.2.1
3
+ version: 0.2.2
4
4
  source_hash: 5a64dfe68b13577dff818fa63ddb6185be360c80b100f205bc586aac39e19e80
5
5
  description: Universal validation and knowledge capture. Detects what changed, runs governance gates, captures knowledge, verifies deployment. Works for any project.
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pre-start-context
3
- version: 0.2.1
3
+ version: 0.2.2
4
4
  source_hash: b7be8434b99d5b189c904263e783d573c82109218725cc31fbd4fa1bf81538b6
5
5
  description: Universal context loader. Discovers any project's stack, architecture, and state at runtime. Reads governance.md for project-specific rules. Works for any language, framework, or deployment target.
6
6
  ---
@@ -47,8 +47,18 @@ function readFrontmatter(filePath) {
47
47
  }
48
48
 
49
49
  /**
50
- * Format a value for YAML frontmatter.
50
+ * Format a value for YAML frontmatter or YAML block scalars.
51
51
  * Quotes strings that contain special characters or could be ambiguous.
52
+ *
53
+ * Rules roughly follow YAML 1.2 plain-scalar constraints:
54
+ * - Leading/trailing whitespace → must quote
55
+ * - Special markers anywhere: : # & * ! | > ' " % @ `
56
+ * - Leading flow indicators: [ ] { } , (would start a flow sequence/map)
57
+ * - Leading dash + space looks like a block sequence entry
58
+ * - Leading ? or ! looks like a YAML tag or complex-key marker
59
+ * - Reserved words that coerce to other types: true/false/null/yes/no/~
60
+ * - Number-like strings
61
+ * - Empty string
52
62
  */
53
63
  function yamlScalar(value) {
54
64
  if (value == null) return '';
@@ -60,15 +70,18 @@ function yamlScalar(value) {
60
70
  return `|\n${indented}`;
61
71
  }
62
72
 
63
- // Characters that require quoting in YAML plain scalar:
64
- // : leading/trailing or followed by space (key separator)
65
- // # comment marker
66
- // special markers: & * ! | > ' " % @ `
67
- // leading/trailing whitespace
68
- // strings that could be misread as other types: true, false, null, yes, no, numbers
73
+ // Control characters (tabs, etc.) must be quoted so they survive round-trip.
74
+ // eslint-disable-next-line no-control-regex
75
+ if (/[\x00-\x1f]/.test(str)) {
76
+ return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\t/g, '\\t').replace(/\r/g, '\\r')}"`;
77
+ }
78
+
69
79
  const needsQuoting =
70
80
  /^[\s]|[\s]$/.test(str) ||
71
81
  /[:#&*!|>'"%@`]/.test(str) ||
82
+ /^[\[\]{},]/.test(str) ||
83
+ /^- /.test(str) ||
84
+ /^[?!]/.test(str) ||
72
85
  /^(true|false|null|yes|no|~)$/i.test(str) ||
73
86
  /^-?\d+(\.\d+)?$/.test(str) ||
74
87
  str === '';
@@ -113,4 +113,4 @@ function syncSkills(targetDir, options = {}) {
113
113
  return result;
114
114
  }
115
115
 
116
- module.exports = { syncSkills };
116
+ module.exports = { syncSkills, isTrustedSource };