@gobing-ai/ts-rule-engine 0.2.6 → 0.2.8

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 (80) hide show
  1. package/README.md +563 -1
  2. package/dist/config/extensions.d.ts +48 -0
  3. package/dist/config/extensions.d.ts.map +1 -0
  4. package/dist/config/extensions.js +67 -0
  5. package/dist/config/loader.d.ts +20 -1
  6. package/dist/config/loader.d.ts.map +1 -1
  7. package/dist/config/loader.js +52 -26
  8. package/dist/engine.d.ts +26 -1
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +79 -0
  11. package/dist/evaluators/exit-code-evaluator.d.ts +1 -1
  12. package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
  13. package/dist/evaluators/exit-code-evaluator.js +22 -9
  14. package/dist/evaluators/forbidden-import-evaluator.d.ts +16 -3
  15. package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
  16. package/dist/evaluators/forbidden-import-evaluator.js +71 -6
  17. package/dist/evaluators/import-boundary-evaluator.d.ts +19 -0
  18. package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -0
  19. package/dist/evaluators/import-boundary-evaluator.js +85 -0
  20. package/dist/evaluators/path-evaluator.d.ts +15 -2
  21. package/dist/evaluators/path-evaluator.d.ts.map +1 -1
  22. package/dist/evaluators/path-evaluator.js +49 -3
  23. package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
  24. package/dist/evaluators/regex-evaluator.js +43 -8
  25. package/dist/evaluators/schema-artifact-evaluator.d.ts +21 -0
  26. package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -0
  27. package/dist/evaluators/schema-artifact-evaluator.js +102 -0
  28. package/dist/evaluators/secrets-scanner-evaluator.d.ts +13 -2
  29. package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
  30. package/dist/evaluators/secrets-scanner-evaluator.js +72 -9
  31. package/dist/evaluators/sg-evaluator.d.ts +19 -0
  32. package/dist/evaluators/sg-evaluator.d.ts.map +1 -0
  33. package/dist/evaluators/sg-evaluator.js +112 -0
  34. package/dist/evaluators/test-location-evaluator.d.ts +14 -1
  35. package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
  36. package/dist/evaluators/test-location-evaluator.js +42 -22
  37. package/dist/evaluators/tsdoc-export-evaluator.js +19 -5
  38. package/dist/fixers/fixers.d.ts +86 -0
  39. package/dist/fixers/fixers.d.ts.map +1 -0
  40. package/dist/fixers/fixers.js +230 -0
  41. package/dist/fixers/test-stub-fixer.d.ts +49 -0
  42. package/dist/fixers/test-stub-fixer.d.ts.map +1 -0
  43. package/dist/fixers/test-stub-fixer.js +91 -0
  44. package/dist/host/builtins.d.ts.map +1 -1
  45. package/dist/host/builtins.js +12 -1
  46. package/dist/host/rule-engine-host.d.ts +3 -0
  47. package/dist/host/rule-engine-host.d.ts.map +1 -1
  48. package/dist/host/rule-engine-host.js +3 -0
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -0
  52. package/dist/resolvers/test-path-resolver.d.ts +72 -0
  53. package/dist/resolvers/test-path-resolver.d.ts.map +1 -0
  54. package/dist/resolvers/test-path-resolver.js +112 -0
  55. package/dist/types.d.ts +36 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js +10 -0
  58. package/package.json +4 -3
  59. package/schemas/preset.schema.json +40 -0
  60. package/schemas/rule-file.schema.json +49 -0
  61. package/src/config/extensions.ts +115 -0
  62. package/src/config/loader.ts +81 -25
  63. package/src/engine.ts +99 -2
  64. package/src/evaluators/exit-code-evaluator.ts +27 -9
  65. package/src/evaluators/forbidden-import-evaluator.ts +101 -7
  66. package/src/evaluators/import-boundary-evaluator.ts +135 -0
  67. package/src/evaluators/path-evaluator.ts +66 -3
  68. package/src/evaluators/regex-evaluator.ts +53 -12
  69. package/src/evaluators/schema-artifact-evaluator.ts +134 -0
  70. package/src/evaluators/secrets-scanner-evaluator.ts +89 -11
  71. package/src/evaluators/sg-evaluator.ts +133 -0
  72. package/src/evaluators/test-location-evaluator.ts +47 -35
  73. package/src/evaluators/tsdoc-export-evaluator.ts +19 -5
  74. package/src/fixers/fixers.ts +294 -0
  75. package/src/fixers/test-stub-fixer.ts +118 -0
  76. package/src/host/builtins.ts +17 -1
  77. package/src/host/rule-engine-host.ts +4 -0
  78. package/src/index.ts +4 -0
  79. package/src/resolvers/test-path-resolver.ts +133 -0
  80. package/src/types.ts +40 -0
@@ -1,9 +1,22 @@
1
1
  import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
- /** Evaluates file or directory existence constraints. */
2
+ /**
3
+ * Evaluates file/directory presence constraints.
4
+ *
5
+ * Two config shapes are supported:
6
+ * - Glob form: `{ must: 'present' | 'absent' }` scoped by the rule's `include` /
7
+ * `exclude` globs. `present` flags each include glob that matches zero files;
8
+ * `absent` flags each in-scope file that exists.
9
+ * - Explicit form: `{ paths: string[], mode?: 'require' | 'forbid' }` — checks the
10
+ * exact paths relative to the workdir (`require` = must exist, `forbid` = must not).
11
+ */
3
12
  export declare class PathEvaluator implements RuleEvaluator {
4
13
  private readonly fs;
5
14
  constructor();
6
- /** Evaluate required or forbidden paths. */
15
+ /** Evaluate required or forbidden paths in either config form. */
7
16
  evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
17
+ /** Glob form driven by `must` + the rule's include/exclude globs. */
18
+ private evaluateGlob;
19
+ /** Explicit form: exact `paths` checked with `mode` require/forbid. */
20
+ private evaluateExplicit;
8
21
  }
9
22
  //# sourceMappingURL=path-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/path-evaluator.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAElB,yDAAyD;AACzD,qBAAa,aAAc,YAAW,aAAa;IAC/C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,4CAA4C;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAgB5F"}
1
+ {"version":3,"file":"path-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/path-evaluator.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;GASG;AACH,qBAAa,aAAc,YAAW,aAAa;IAC/C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,kEAAkE;IAC5D,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IASzF,qEAAqE;YACvD,YAAY;IAuC1B,uEAAuE;YACzD,gBAAgB;CAmBjC"}
@@ -1,15 +1,61 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { NodeFileSystem } from '@gobing-ai/ts-runtime';
3
3
  import { createFinding, } from '../types.js';
4
- /** Evaluates file or directory existence constraints. */
4
+ import { discoverFiles, matchesGlob } from './file-utils.js';
5
+ /**
6
+ * Evaluates file/directory presence constraints.
7
+ *
8
+ * Two config shapes are supported:
9
+ * - Glob form: `{ must: 'present' | 'absent' }` scoped by the rule's `include` /
10
+ * `exclude` globs. `present` flags each include glob that matches zero files;
11
+ * `absent` flags each in-scope file that exists.
12
+ * - Explicit form: `{ paths: string[], mode?: 'require' | 'forbid' }` — checks the
13
+ * exact paths relative to the workdir (`require` = must exist, `forbid` = must not).
14
+ */
5
15
  export class PathEvaluator {
6
16
  fs;
7
17
  constructor() {
8
18
  this.fs = new NodeFileSystem();
9
19
  }
10
- /** Evaluate required or forbidden paths. */
20
+ /** Evaluate required or forbidden paths in either config form. */
11
21
  async evaluate(rule, context) {
12
22
  const config = rule.evaluator.config ?? {};
23
+ const must = config.must;
24
+ if (must === 'present' || must === 'absent') {
25
+ return this.evaluateGlob(rule, context, must);
26
+ }
27
+ return this.evaluateExplicit(rule, context, config);
28
+ }
29
+ /** Glob form driven by `must` + the rule's include/exclude globs. */
30
+ async evaluateGlob(rule, context, must) {
31
+ const include = rule.include ?? ['**'];
32
+ const exclude = rule.exclude ?? [];
33
+ const files = await discoverFiles({ workdir: context.workdir });
34
+ const inScope = (file) => include.some((glob) => matchesGlob(file, glob)) && !exclude.some((glob) => matchesGlob(file, glob));
35
+ const findings = [];
36
+ if (must === 'present') {
37
+ for (const pattern of include) {
38
+ const present = files.some((file) => matchesGlob(file, pattern) && !exclude.some((g) => matchesGlob(file, g)));
39
+ if (!present) {
40
+ findings.push(createFinding(rule, `expected files matching "${pattern}", but none found`, pattern, {
41
+ code: 'path:missing',
42
+ }));
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ for (const file of files) {
48
+ if (inScope(file)) {
49
+ findings.push(createFinding(rule, 'file should be absent (forbidden by rule)', file, {
50
+ code: 'path:forbidden',
51
+ }));
52
+ }
53
+ }
54
+ }
55
+ return { findings, fixes: [] };
56
+ }
57
+ /** Explicit form: exact `paths` checked with `mode` require/forbid. */
58
+ async evaluateExplicit(rule, context, config) {
13
59
  const paths = arrayConfig(config, 'paths');
14
60
  const mode = stringConfig(config, 'mode', 'require');
15
61
  const findings = [];
@@ -31,7 +77,7 @@ function arrayConfig(config, key) {
31
77
  return value;
32
78
  if (typeof value === 'string')
33
79
  return [value];
34
- throw new Error(`path evaluator requires string[] config "${key}"`);
80
+ throw new Error(`path evaluator requires config "${key}" (string[]) or "must" (present|absent)`);
35
81
  }
36
82
  function stringConfig(config, key, fallback) {
37
83
  const value = config[key];
@@ -1 +1 @@
1
- {"version":3,"file":"regex-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/regex-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB,qEAAqE;AACrE,qBAAa,cAAe,YAAW,aAAa;;IAGhD,4DAA4D;IACtD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA2B5F"}
1
+ {"version":3,"file":"regex-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/regex-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB,qEAAqE;AACrE,qBAAa,cAAe,YAAW,aAAa;;IAGhD,4DAA4D;IACtD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAuC5F"}
@@ -6,26 +6,61 @@ export class RegexEvaluator {
6
6
  /** Evaluate regex-based presence or absence constraints. */
7
7
  async evaluate(rule, context) {
8
8
  const config = rule.evaluator.config ?? {};
9
- const pattern = stringConfig(config, 'pattern');
9
+ const { pattern, flags } = normalizePattern(stringConfig(config, 'pattern'), stringConfig(config, 'flags', ''), config.multiline === true);
10
10
  const mode = stringConfig(config, 'mode', 'forbid');
11
- const flags = stringConfig(config, 'flags', 'm');
12
11
  const regex = new RegExp(pattern, flags);
13
12
  const files = await discoverFiles({ workdir: context.workdir, include: rule.include, exclude: rule.exclude });
14
13
  const findings = [];
15
14
  for (const file of files) {
16
15
  const content = await readWorkdirFile(context.workdir, file);
17
- regex.lastIndex = 0;
18
- const match = regex.exec(content);
19
- if (mode === 'require' && match === null) {
20
- findings.push(createFinding(rule, `Required pattern not found: ${pattern}`, file, { code: 'regex:missing' }));
16
+ if (mode === 'require') {
17
+ regex.lastIndex = 0;
18
+ if (!regex.test(content)) {
19
+ findings.push(createFinding(rule, `required pattern not found: ${pattern}`, file, { code: 'regex:missing' }));
20
+ }
21
+ continue;
21
22
  }
22
- if (mode !== 'require' && match !== null) {
23
- findings.push(createFinding(rule, `Forbidden pattern found: ${pattern}`, file, { code: 'regex:found' }));
23
+ // forbid: report each matching line so findings carry precise locations.
24
+ for (const [index, line] of content.split('\n').entries()) {
25
+ regex.lastIndex = 0;
26
+ if (regex.test(line)) {
27
+ findings.push(createFinding(rule, `forbidden pattern found: ${pattern}`, file, {
28
+ line: index + 1,
29
+ code: 'regex:found',
30
+ }));
31
+ }
24
32
  }
25
33
  }
26
34
  return { findings, fixes: [] };
27
35
  }
28
36
  }
37
+ /**
38
+ * Normalize a pattern + flags for JS `RegExp`.
39
+ *
40
+ * Accepts a leading `(?i)`/`(?im)` inline flag group (ripgrep/PCRE style) and
41
+ * folds it into the JS flags. `multiline` adds the `s` (dotAll) flag so `.`
42
+ * spans newlines, matching the old `--multiline` behavior. `m` is always set so
43
+ * `^`/`$` work per line.
44
+ */
45
+ function normalizePattern(rawPattern, rawFlags, multiline) {
46
+ const flagSet = new Set(['m']);
47
+ for (const flag of rawFlags) {
48
+ if ('gimsuy'.includes(flag))
49
+ flagSet.add(flag);
50
+ }
51
+ let pattern = rawPattern;
52
+ const inline = /^\(\?([a-z]+)\)/.exec(pattern);
53
+ if (inline) {
54
+ for (const flag of inline[1] ?? '') {
55
+ if ('imsu'.includes(flag))
56
+ flagSet.add(flag);
57
+ }
58
+ pattern = pattern.slice(inline[0].length);
59
+ }
60
+ if (multiline)
61
+ flagSet.add('s');
62
+ return { pattern, flags: [...flagSet].join('') };
63
+ }
29
64
  function stringConfig(config, key, fallback) {
30
65
  const value = config[key];
31
66
  if (typeof value === 'string')
@@ -0,0 +1,21 @@
1
+ import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
+ /**
3
+ * Evaluates JSON schema artifact files for structural integrity.
4
+ *
5
+ * Pure JS — no subprocess. Validates JSON validity, title, required properties,
6
+ * `$defs` / `definitions` entries, and the presence of a top-level `required` array.
7
+ *
8
+ * ## Options (in `evaluator.config`)
9
+ * - `file` — path to the JSON file relative to the workdir (required).
10
+ * - `requiredTitle` — expected `title` value (optional).
11
+ * - `requiredProperties` — top-level `properties` keys that must exist (optional).
12
+ * - `requiredDefs` — `$defs` or `definitions` keys that must exist (optional).
13
+ * - `requireRequiredArray` — enforce that `required` is a non-empty array (default: `false`).
14
+ */
15
+ export declare class SchemaArtifactEvaluator implements RuleEvaluator {
16
+ private readonly fs;
17
+ constructor();
18
+ /** Evaluate the configured JSON schema artifact. */
19
+ evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
20
+ }
21
+ //# sourceMappingURL=schema-artifact-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-artifact-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/schema-artifact-evaluator.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;GAYG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,oDAAoD;IAC9C,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAiG5F"}
@@ -0,0 +1,102 @@
1
+ import { resolve } from 'node:path';
2
+ import { NodeFileSystem } from '@gobing-ai/ts-runtime';
3
+ import { createFinding, } from '../types.js';
4
+ /**
5
+ * Evaluates JSON schema artifact files for structural integrity.
6
+ *
7
+ * Pure JS — no subprocess. Validates JSON validity, title, required properties,
8
+ * `$defs` / `definitions` entries, and the presence of a top-level `required` array.
9
+ *
10
+ * ## Options (in `evaluator.config`)
11
+ * - `file` — path to the JSON file relative to the workdir (required).
12
+ * - `requiredTitle` — expected `title` value (optional).
13
+ * - `requiredProperties` — top-level `properties` keys that must exist (optional).
14
+ * - `requiredDefs` — `$defs` or `definitions` keys that must exist (optional).
15
+ * - `requireRequiredArray` — enforce that `required` is a non-empty array (default: `false`).
16
+ */
17
+ export class SchemaArtifactEvaluator {
18
+ fs;
19
+ constructor() {
20
+ this.fs = new NodeFileSystem();
21
+ }
22
+ /** Evaluate the configured JSON schema artifact. */
23
+ async evaluate(rule, context) {
24
+ const config = rule.evaluator.config ?? {};
25
+ const file = config.file;
26
+ if (typeof file !== 'string' || file.length === 0) {
27
+ throw new Error('schema-artifact evaluator requires string config "file"');
28
+ }
29
+ const requiredTitle = typeof config.requiredTitle === 'string' ? config.requiredTitle : undefined;
30
+ const requiredProperties = stringArray(config.requiredProperties);
31
+ const requiredDefs = stringArray(config.requiredDefs);
32
+ const requireRequiredArray = config.requireRequiredArray === true;
33
+ // Check existence.
34
+ const absolutePath = resolve(context.workdir, file);
35
+ const exists = await this.fs.exists(absolutePath);
36
+ if (!exists) {
37
+ return {
38
+ findings: [
39
+ createFinding(rule, 'schema artifact not found', file, {
40
+ code: 'schema-artifact:missing',
41
+ }),
42
+ ],
43
+ fixes: [],
44
+ };
45
+ }
46
+ // Read and parse.
47
+ let schema;
48
+ try {
49
+ const raw = await this.fs.readFile(absolutePath);
50
+ schema = JSON.parse(raw);
51
+ }
52
+ catch (err) {
53
+ return {
54
+ findings: [
55
+ createFinding(rule, `invalid JSON: ${err.message}`, file, {
56
+ code: 'schema-artifact:invalid',
57
+ }),
58
+ ],
59
+ fixes: [],
60
+ };
61
+ }
62
+ const findings = [];
63
+ // Validate title.
64
+ if (requiredTitle !== undefined && schema.title !== requiredTitle) {
65
+ findings.push(createFinding(rule, `${file} title expected '${requiredTitle}', got '${schema.title ?? '(missing)'}'`, file, { code: 'schema-artifact:violation' }));
66
+ }
67
+ // Validate required array.
68
+ if (requireRequiredArray && !Array.isArray(schema.required)) {
69
+ findings.push(createFinding(rule, `${file} missing 'required' array at top level`, file, {
70
+ code: 'schema-artifact:violation',
71
+ }));
72
+ }
73
+ // Validate properties.
74
+ if (requiredProperties !== undefined) {
75
+ const props = schema.properties;
76
+ for (const prop of requiredProperties) {
77
+ if (!props || !(prop in props)) {
78
+ findings.push(createFinding(rule, `${file} missing properties.${prop}`, file, {
79
+ code: 'schema-artifact:violation',
80
+ }));
81
+ }
82
+ }
83
+ }
84
+ // Validate $defs / definitions.
85
+ if (requiredDefs !== undefined) {
86
+ const defs = schema.$defs ??
87
+ schema.definitions;
88
+ for (const def of requiredDefs) {
89
+ if (!defs || !(def in defs)) {
90
+ findings.push(createFinding(rule, `${file} missing $defs.${def}`, file, {
91
+ code: 'schema-artifact:violation',
92
+ }));
93
+ }
94
+ }
95
+ }
96
+ return { findings, fixes: [] };
97
+ }
98
+ }
99
+ /** Return a string array if value is a string array, otherwise undefined. */
100
+ function stringArray(value) {
101
+ return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : undefined;
102
+ }
@@ -1,8 +1,19 @@
1
1
  import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
- /** Scans text files for high-confidence secret-like tokens. */
2
+ /** Built-in secret category names. */
3
+ export type SecretsCategory = 'api-key' | 'private-key' | 'password' | 'token' | 'connection-string';
4
+ /**
5
+ * Scans text files for secret-like tokens.
6
+ *
7
+ * Config (`evaluator.config`, all optional):
8
+ * - `categories`: subset of built-in categories to scan (default: all five —
9
+ * `api-key`, `private-key`, `password`, `token`, `connection-string`).
10
+ * - `customPatterns`: extra `{ name, pattern }` entries to scan for.
11
+ * - `scope`: `{ include, exclude }` globs; falls back to the rule's `include` /
12
+ * `exclude` when omitted.
13
+ */
3
14
  export declare class SecretsScannerEvaluator implements RuleEvaluator {
4
15
  constructor();
5
- /** Evaluate source files against bundled secret patterns. */
16
+ /** Evaluate files against the selected secret categories and custom patterns. */
6
17
  evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
7
18
  }
8
19
  //# sourceMappingURL=secrets-scanner-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secrets-scanner-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/secrets-scanner-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB,+DAA+D;AAC/D,qBAAa,uBAAwB,YAAW,aAAa;;IAGzD,6DAA6D;IACvD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAmB5F"}
1
+ {"version":3,"file":"secrets-scanner-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/secrets-scanner-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB,sCAAsC;AACtC,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,mBAAmB,CAAC;AA0BrG;;;;;;;;;GASG;AACH,qBAAa,uBAAwB,YAAW,aAAa;;IAGzD,iFAAiF;IAC3E,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA4B5F"}
@@ -1,24 +1,87 @@
1
1
  import { createFinding, } from '../types.js';
2
2
  import { discoverFiles, readWorkdirFile } from './file-utils.js';
3
- /** Scans text files for high-confidence secret-like tokens. */
3
+ // Patterns are assembled from a keyword group plus a value-shape suffix rather
4
+ // than written as literal `keyword: "value"` lines, so this source file does not
5
+ // trip the secrets-scanner when it scans itself.
6
+ const ASSIGN = '\\s*[=:]\\s*';
7
+ const QUOTED = (body) => `["'\`]${body}["'\`]`;
8
+ const keyworded = (keywords, value) => `(?i)(${keywords})${ASSIGN}${QUOTED(value)}`;
9
+ /** Built-in category → regex source. A leading `(?i)` is folded into the JS `i` flag. */
10
+ const BUILTIN_PATTERNS = {
11
+ 'api-key': `${keyworded('api[_-]?key|apikey|api_secret|access[_-]?token|auth[_-]?token|secret[_-]?key', '[A-Za-z0-9_/+=.-]{8,}')}|sk-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}`,
12
+ 'private-key': 'PRIVATE[_-]?KEY|-----BEGIN\\s+(?:RSA |OPENSSH |EC )?PRIVATE\\s+KEY',
13
+ password: keyworded('passw[o]rd|passwd|pwd|pass', '[^"\'`]{4,}'),
14
+ token: keyworded('bearer[_-]?token|refresh[_-]?token|session[_-]?token', '[A-Za-z0-9_\\-./+=]{8,}'),
15
+ 'connection-string': '(?i)(mongodb|postgres|mysql|redis)://[^\\s"\'`]{8,}',
16
+ };
17
+ const ALL_CATEGORIES = Object.keys(BUILTIN_PATTERNS);
18
+ /**
19
+ * Scans text files for secret-like tokens.
20
+ *
21
+ * Config (`evaluator.config`, all optional):
22
+ * - `categories`: subset of built-in categories to scan (default: all five —
23
+ * `api-key`, `private-key`, `password`, `token`, `connection-string`).
24
+ * - `customPatterns`: extra `{ name, pattern }` entries to scan for.
25
+ * - `scope`: `{ include, exclude }` globs; falls back to the rule's `include` /
26
+ * `exclude` when omitted.
27
+ */
4
28
  export class SecretsScannerEvaluator {
5
29
  constructor() { }
6
- /** Evaluate source files against bundled secret patterns. */
30
+ /** Evaluate files against the selected secret categories and custom patterns. */
7
31
  async evaluate(rule, context) {
8
- const files = await discoverFiles({ workdir: context.workdir, include: rule.include, exclude: rule.exclude });
32
+ const config = rule.evaluator.config ?? {};
33
+ const patterns = buildPatterns(config);
34
+ const scope = config.scope;
35
+ const include = stringArray(scope?.include) ?? rule.include;
36
+ const exclude = stringArray(scope?.exclude) ?? rule.exclude;
37
+ const files = await discoverFiles({ workdir: context.workdir, include, exclude });
9
38
  const findings = [];
10
- const secretPattern = /(sk-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY)/;
11
39
  for (const file of files) {
12
40
  const lines = (await readWorkdirFile(context.workdir, file)).split('\n');
13
41
  for (const [index, line] of lines.entries()) {
14
- if (secretPattern.test(line)) {
15
- findings.push(createFinding(rule, 'Potential secret token found', file, {
16
- line: index + 1,
17
- code: 'secret:token',
18
- }));
42
+ for (const pattern of patterns) {
43
+ pattern.regex.lastIndex = 0;
44
+ if (pattern.regex.test(line)) {
45
+ findings.push(createFinding(rule, `potential secret (${pattern.label})`, file, {
46
+ line: index + 1,
47
+ code: `secret:${pattern.label}`,
48
+ }));
49
+ break;
50
+ }
19
51
  }
20
52
  }
21
53
  }
22
54
  return { findings, fixes: [] };
23
55
  }
24
56
  }
57
+ /** Compile the selected built-in categories plus any custom patterns. */
58
+ function buildPatterns(config) {
59
+ const categories = stringArray(config.categories) ?? ALL_CATEGORIES;
60
+ const patterns = [];
61
+ for (const category of categories) {
62
+ const source = BUILTIN_PATTERNS[category];
63
+ if (source !== undefined)
64
+ patterns.push({ regex: compile(source), label: category });
65
+ }
66
+ const custom = config.customPatterns;
67
+ if (Array.isArray(custom)) {
68
+ for (const entry of custom) {
69
+ if (typeof entry.name === 'string' && typeof entry.pattern === 'string') {
70
+ patterns.push({ regex: compile(entry.pattern), label: entry.name });
71
+ }
72
+ }
73
+ }
74
+ return patterns;
75
+ }
76
+ /** Compile a pattern, folding a leading `(?i)` group into the JS `i` flag. */
77
+ function compile(source) {
78
+ const inline = /^\(\?([a-z]+)\)/.exec(source);
79
+ if (inline) {
80
+ const flags = [...(inline[1] ?? '')].filter((flag) => 'imsu'.includes(flag)).join('');
81
+ return new RegExp(source.slice(inline[0].length), flags);
82
+ }
83
+ return new RegExp(source);
84
+ }
85
+ function stringArray(value) {
86
+ return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : undefined;
87
+ }
@@ -0,0 +1,19 @@
1
+ import { type ProcessExecutor } from '@gobing-ai/ts-runtime';
2
+ import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
3
+ /**
4
+ * Evaluates source code against an ast-grep pattern using the `sg` CLI.
5
+ *
6
+ * ## Options (in `evaluator.config`)
7
+ * - `pattern` — ast-grep pattern to search for (required).
8
+ * - `language` — language for ast-grep parsing (default: `typescript`).
9
+ *
10
+ * Include globs from the rule are forwarded to sg via `--glob` arguments.
11
+ * Exclude globs from the rule are applied in-process after parsing sg output.
12
+ */
13
+ export declare class SgEvaluator implements RuleEvaluator {
14
+ private readonly executor;
15
+ constructor(executor?: ProcessExecutor);
16
+ /** Run sg and emit one finding per matched AST node. */
17
+ evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
18
+ }
19
+ //# sourceMappingURL=sg-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sg-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/sg-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;GASG;AACH,qBAAa,WAAY,YAAW,aAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;gBAE/B,QAAQ,GAAE,eAA2C;IAIjE,wDAAwD;IAClD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAgD5F"}
@@ -0,0 +1,112 @@
1
+ import { NodeProcessExecutor } from '@gobing-ai/ts-runtime';
2
+ import { createFinding, } from '../types.js';
3
+ import { matchesGlob } from './file-utils.js';
4
+ /**
5
+ * Evaluates source code against an ast-grep pattern using the `sg` CLI.
6
+ *
7
+ * ## Options (in `evaluator.config`)
8
+ * - `pattern` — ast-grep pattern to search for (required).
9
+ * - `language` — language for ast-grep parsing (default: `typescript`).
10
+ *
11
+ * Include globs from the rule are forwarded to sg via `--glob` arguments.
12
+ * Exclude globs from the rule are applied in-process after parsing sg output.
13
+ */
14
+ export class SgEvaluator {
15
+ executor;
16
+ constructor(executor = new NodeProcessExecutor()) {
17
+ this.executor = executor;
18
+ }
19
+ /** Run sg and emit one finding per matched AST node. */
20
+ async evaluate(rule, context) {
21
+ const config = rule.evaluator.config ?? {};
22
+ const pattern = config.pattern;
23
+ if (typeof pattern !== 'string' || pattern.length === 0) {
24
+ throw new Error('sg evaluator requires string config "pattern"');
25
+ }
26
+ const language = typeof config.language === 'string' ? config.language : 'typescript';
27
+ const include = rule.include ?? [];
28
+ const exclude = rule.exclude ?? [];
29
+ const args = ['run', '--pattern', pattern, '--lang', language, '--json'];
30
+ for (const glob of include) {
31
+ args.push(`--glob=${glob}`);
32
+ }
33
+ const result = await this.executor.run({
34
+ command: 'sg',
35
+ args,
36
+ cwd: context.workdir,
37
+ timeout: 60_000,
38
+ rejectOnError: false,
39
+ label: 'sg',
40
+ });
41
+ const stdout = result.stdout.trim();
42
+ if (stdout.length === 0) {
43
+ if (result.exitCode !== 0 && result.exitCode !== null && result.stderr.trim().length > 0) {
44
+ throw new Error(`sg failed: ${result.stderr.trim()}`);
45
+ }
46
+ return { findings: [], fixes: [] };
47
+ }
48
+ const matches = parseSgJson(stdout);
49
+ const findings = [];
50
+ for (const match of matches) {
51
+ if (exclude.some((glob) => matchesGlob(match.file, glob)))
52
+ continue;
53
+ findings.push(createFinding(rule, 'matched sg pattern', match.file, {
54
+ line: match.line,
55
+ code: 'sg:match',
56
+ }));
57
+ }
58
+ return { findings, fixes: [] };
59
+ }
60
+ }
61
+ /**
62
+ * Parse `sg --json` output.
63
+ *
64
+ * Handles both a JSON array (sg >= 0.40) and newline-delimited JSON objects
65
+ * (older sg or `--json=stream`). Returns workdir-relative file paths as-is
66
+ * since sg emits paths relative to cwd.
67
+ */
68
+ function parseSgJson(stdout) {
69
+ // Try JSON array first (newer sg).
70
+ try {
71
+ const parsed = JSON.parse(stdout);
72
+ if (Array.isArray(parsed)) {
73
+ return parsed.flatMap((event) => {
74
+ const file = typeof event.file === 'string' ? event.file : '';
75
+ if (!file)
76
+ return [];
77
+ return [
78
+ {
79
+ file,
80
+ line: (event.range?.start?.line ?? 0) + 1,
81
+ text: typeof event.text === 'string' ? event.text.trim() : '',
82
+ },
83
+ ];
84
+ });
85
+ }
86
+ }
87
+ catch {
88
+ // Fall through to line-delimited parsing.
89
+ }
90
+ // Fallback: newline-delimited JSON.
91
+ const results = [];
92
+ for (const line of stdout.split('\n')) {
93
+ const trimmed = line.trim();
94
+ if (!trimmed)
95
+ continue;
96
+ try {
97
+ const event = JSON.parse(trimmed);
98
+ const file = typeof event.file === 'string' ? event.file : '';
99
+ if (!file)
100
+ continue;
101
+ results.push({
102
+ file,
103
+ line: (event.range?.start?.line ?? 0) + 1,
104
+ text: typeof event.text === 'string' ? event.text.trim() : '',
105
+ });
106
+ }
107
+ catch {
108
+ // Skip unparseable lines.
109
+ }
110
+ }
111
+ return results;
112
+ }
@@ -1,3 +1,5 @@
1
+ import type { CapabilityRegistry } from '../host/capability-registry';
2
+ import type { TestPathResolver } from '../resolvers/test-path-resolver';
1
3
  import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
4
  /**
3
5
  * Evaluator that enforces where test files live and, optionally, that every
@@ -7,13 +9,24 @@ import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type
7
9
  * - `expected`: glob the test files must match (required)
8
10
  * - `forbid`: globs where tests must not live (e.g. `**\/__tests__/**`)
9
11
  * - `requireCorrespondingTest`: when true, flags source files (from `rule.include`)
10
- * that lack a test at the TypeScript-conventional path
12
+ * that lack a test at the resolver's conventional path
13
+ * - `resolver`: language resolver name (`typescript` default, or `python`/`go`/`rust`)
11
14
  *
12
15
  * Discovery walks the workdir and applies `**` globs precisely, so it stays
13
16
  * self-contained (no `rg --files` shell-out).
14
17
  */
15
18
  export declare class TestLocationEvaluator implements RuleEvaluator {
19
+ private readonly resolvers?;
20
+ /** Optional resolver registry; when absent, the TypeScript convention is used. */
21
+ constructor(resolvers?: CapabilityRegistry<TestPathResolver> | undefined);
16
22
  /** Evaluate test-file placement and optional coverage of source files. */
17
23
  evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
24
+ /**
25
+ * Pick the resolver named in config (default `typescript`).
26
+ *
27
+ * Falls back to the built-in TypeScript convention when no registry was
28
+ * injected; throws if a named resolver is requested but not registered.
29
+ */
30
+ private selectResolver;
18
31
  }
19
32
  //# sourceMappingURL=test-location-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-location-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/test-location-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAUlB;;;;;;;;;;;;GAYG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACvD,0EAA0E;IACpE,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAiE5F"}
1
+ {"version":3,"file":"test-location-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/test-location-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAWlB;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IAE3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IADvC,kFAAkF;gBACrD,SAAS,CAAC,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,YAAA;IAE7E,0EAA0E;IACpE,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAyDzF;;;;;OAKG;IACH,OAAO,CAAC,cAAc;CASzB"}