toolmoat 0.0.0-stage → 0.1.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +80 -0
  5. package/dist/clients/configs.d.ts +124 -0
  6. package/dist/clients/configs.js +403 -0
  7. package/dist/commands/approve.d.ts +15 -0
  8. package/dist/commands/approve.js +128 -0
  9. package/dist/commands/diff.d.ts +6 -0
  10. package/dist/commands/diff.js +114 -0
  11. package/dist/commands/pins.d.ts +18 -0
  12. package/dist/commands/pins.js +88 -0
  13. package/dist/commands/proxy.d.ts +24 -0
  14. package/dist/commands/proxy.js +144 -0
  15. package/dist/commands/rules.d.ts +15 -0
  16. package/dist/commands/rules.js +173 -0
  17. package/dist/commands/scan.d.ts +57 -0
  18. package/dist/commands/scan.js +323 -0
  19. package/dist/commands/unwrap.d.ts +5 -0
  20. package/dist/commands/unwrap.js +69 -0
  21. package/dist/commands/wrap.d.ts +131 -0
  22. package/dist/commands/wrap.js +518 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.js +13 -0
  25. package/dist/patterns/indicators.d.ts +41 -0
  26. package/dist/patterns/indicators.js +85 -0
  27. package/dist/patterns/secrets.d.ts +20 -0
  28. package/dist/patterns/secrets.js +70 -0
  29. package/dist/proxy/audit.d.ts +53 -0
  30. package/dist/proxy/audit.js +79 -0
  31. package/dist/proxy/canonical.d.ts +13 -0
  32. package/dist/proxy/canonical.js +34 -0
  33. package/dist/proxy/diff.d.ts +22 -0
  34. package/dist/proxy/diff.js +124 -0
  35. package/dist/proxy/fingerprint.d.ts +34 -0
  36. package/dist/proxy/fingerprint.js +140 -0
  37. package/dist/proxy/firewall.d.ts +132 -0
  38. package/dist/proxy/firewall.js +665 -0
  39. package/dist/proxy/framing.d.ts +45 -0
  40. package/dist/proxy/framing.js +86 -0
  41. package/dist/proxy/lockfile.d.ts +58 -0
  42. package/dist/proxy/lockfile.js +258 -0
  43. package/dist/proxy/names.d.ts +27 -0
  44. package/dist/proxy/names.js +175 -0
  45. package/dist/proxy/policy.d.ts +72 -0
  46. package/dist/proxy/policy.js +86 -0
  47. package/dist/proxy/proxy.d.ts +18 -0
  48. package/dist/proxy/proxy.js +237 -0
  49. package/dist/proxy/spawn.d.ts +51 -0
  50. package/dist/proxy/spawn.js +110 -0
  51. package/dist/report/index.d.ts +35 -0
  52. package/dist/report/index.js +25 -0
  53. package/dist/report/json.d.ts +22 -0
  54. package/dist/report/json.js +40 -0
  55. package/dist/report/markdown.d.ts +12 -0
  56. package/dist/report/markdown.js +87 -0
  57. package/dist/report/sarif.d.ts +89 -0
  58. package/dist/report/sarif.js +142 -0
  59. package/dist/report/text.d.ts +16 -0
  60. package/dist/report/text.js +206 -0
  61. package/dist/rules/code.d.ts +15 -0
  62. package/dist/rules/code.js +729 -0
  63. package/dist/rules/config.d.ts +3 -0
  64. package/dist/rules/config.js +2164 -0
  65. package/dist/rules/index.d.ts +3 -0
  66. package/dist/rules/index.js +8 -0
  67. package/dist/rules/secrets.d.ts +2 -0
  68. package/dist/rules/secrets.js +108 -0
  69. package/dist/rules/text.d.ts +2 -0
  70. package/dist/rules/text.js +1698 -0
  71. package/dist/scan/discover.d.ts +37 -0
  72. package/dist/scan/discover.js +525 -0
  73. package/dist/scan/engine.d.ts +24 -0
  74. package/dist/scan/engine.js +111 -0
  75. package/dist/scan/ignore.d.ts +28 -0
  76. package/dist/scan/ignore.js +46 -0
  77. package/dist/types.d.ts +98 -0
  78. package/dist/types.js +25 -0
  79. package/dist/util/colors.d.ts +12 -0
  80. package/dist/util/colors.js +24 -0
  81. package/dist/util/frontmatter.d.ts +20 -0
  82. package/dist/util/frontmatter.js +136 -0
  83. package/dist/util/glob.d.ts +7 -0
  84. package/dist/util/glob.js +44 -0
  85. package/dist/util/hash.d.ts +3 -0
  86. package/dist/util/hash.js +24 -0
  87. package/dist/util/jsonc.d.ts +14 -0
  88. package/dist/util/jsonc.js +113 -0
  89. package/dist/util/paths.d.ts +9 -0
  90. package/dist/util/paths.js +37 -0
  91. package/dist/util/text.d.ts +29 -0
  92. package/dist/util/text.js +96 -0
  93. package/dist/util/toml-lite.d.ts +40 -0
  94. package/dist/util/toml-lite.js +507 -0
  95. package/dist/version.d.ts +1 -0
  96. package/dist/version.js +1 -0
  97. package/package.json +57 -4
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Parses JSON that may contain // and /* *\/ comments and trailing commas, as
3
+ * VS Code style config files do. Returns undefined instead of throwing.
4
+ */
5
+ export function parseJsonc(text) {
6
+ const source = text.startsWith('') ? text.slice(1) : text;
7
+ try {
8
+ return JSON.parse(source);
9
+ }
10
+ catch {
11
+ // fall through to the lenient path
12
+ }
13
+ try {
14
+ return JSON.parse(stripTrailingCommas(stripComments(source)));
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /** Replaces comments with spaces so character offsets stay the same. */
21
+ export function stripComments(text) {
22
+ let out = '';
23
+ let inString = false;
24
+ for (let i = 0; i < text.length; i++) {
25
+ const ch = text[i];
26
+ const next = text[i + 1];
27
+ if (inString) {
28
+ out += ch;
29
+ if (ch === '\\') {
30
+ out += next ?? '';
31
+ i++;
32
+ }
33
+ else if (ch === '"') {
34
+ inString = false;
35
+ }
36
+ }
37
+ else if (ch === '"') {
38
+ inString = true;
39
+ out += ch;
40
+ }
41
+ else if (ch === '/' && next === '/') {
42
+ while (i < text.length && text[i] !== '\n') {
43
+ out += ' ';
44
+ i++;
45
+ }
46
+ if (i < text.length)
47
+ out += '\n';
48
+ }
49
+ else if (ch === '/' && next === '*') {
50
+ out += ' ';
51
+ i += 2;
52
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) {
53
+ out += text[i] === '\n' ? '\n' : ' ';
54
+ i++;
55
+ }
56
+ out += ' ';
57
+ i++;
58
+ }
59
+ else {
60
+ out += ch;
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ function stripTrailingCommas(text) {
66
+ let out = '';
67
+ let inString = false;
68
+ for (let i = 0; i < text.length; i++) {
69
+ const ch = text[i];
70
+ if (inString) {
71
+ out += ch;
72
+ if (ch === '\\') {
73
+ out += text[i + 1] ?? '';
74
+ i++;
75
+ }
76
+ else if (ch === '"') {
77
+ inString = false;
78
+ }
79
+ continue;
80
+ }
81
+ if (ch === '"') {
82
+ inString = true;
83
+ out += ch;
84
+ continue;
85
+ }
86
+ if (ch === ',') {
87
+ let j = i + 1;
88
+ while (j < text.length && /\s/.test(text[j]))
89
+ j++;
90
+ if (text[j] === '}' || text[j] === ']') {
91
+ out += ' ';
92
+ continue;
93
+ }
94
+ }
95
+ out += ch;
96
+ }
97
+ return out;
98
+ }
99
+ /**
100
+ * Best-effort offset of a JSON string value (or key) in the raw text, used to
101
+ * point findings at the right line. Searches for the JSON-encoded form first,
102
+ * then the raw value. Returns -1 when not found.
103
+ */
104
+ export function indexOfJsonString(text, value, from = 0) {
105
+ const encoded = JSON.stringify(value);
106
+ const exact = text.indexOf(encoded, from);
107
+ if (exact !== -1)
108
+ return exact;
109
+ return text.indexOf(value, from);
110
+ }
111
+ export function isRecord(value) {
112
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
113
+ }
@@ -0,0 +1,9 @@
1
+ export declare function homeDir(): string;
2
+ /** Expands a leading ~ to the user's home directory. */
3
+ export declare function expandHome(p: string): string;
4
+ /** Where toolmoat keeps its lockfile and audit logs. Override with TOOLMOAT_HOME. */
5
+ export declare function toolmoatHome(): string;
6
+ /** Forward-slash path relative to root when the file is inside it, otherwise the absolute path. */
7
+ export declare function toDisplayPath(absPath: string, root?: string): string;
8
+ /** Platform-specific application-data directory (APPDATA on Windows, ~/Library/Application Support on macOS, XDG_CONFIG_HOME elsewhere). */
9
+ export declare function appDataDir(): string;
@@ -0,0 +1,37 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export function homeDir() {
4
+ return process.env.TOOLMOAT_TEST_USER_HOME ?? os.homedir();
5
+ }
6
+ /** Expands a leading ~ to the user's home directory. */
7
+ export function expandHome(p) {
8
+ if (p === '~')
9
+ return homeDir();
10
+ if (p.startsWith('~/') || p.startsWith('~\\'))
11
+ return path.join(homeDir(), p.slice(2));
12
+ return p;
13
+ }
14
+ /** Where toolmoat keeps its lockfile and audit logs. Override with TOOLMOAT_HOME. */
15
+ export function toolmoatHome() {
16
+ return process.env.TOOLMOAT_HOME ?? path.join(homeDir(), '.toolmoat');
17
+ }
18
+ /** Forward-slash path relative to root when the file is inside it, otherwise the absolute path. */
19
+ export function toDisplayPath(absPath, root) {
20
+ if (absPath.includes('://'))
21
+ return absPath;
22
+ let shown = absPath;
23
+ if (root) {
24
+ const rel = path.relative(root, absPath);
25
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel))
26
+ shown = rel;
27
+ }
28
+ return shown.split(path.sep).join('/');
29
+ }
30
+ /** Platform-specific application-data directory (APPDATA on Windows, ~/Library/Application Support on macOS, XDG_CONFIG_HOME elsewhere). */
31
+ export function appDataDir() {
32
+ if (process.platform === 'win32')
33
+ return process.env.APPDATA ?? path.join(homeDir(), 'AppData', 'Roaming');
34
+ if (process.platform === 'darwin')
35
+ return path.join(homeDir(), 'Library', 'Application Support');
36
+ return process.env.XDG_CONFIG_HOME ?? path.join(homeDir(), '.config');
37
+ }
@@ -0,0 +1,29 @@
1
+ /** Converts a character offset into 1-based line and column numbers. */
2
+ export declare function lineColAt(content: string, index: number): {
3
+ line: number;
4
+ column: number;
5
+ };
6
+ /** Returns the text of a 1-based line without its line terminator, or '' when out of range. */
7
+ export declare function lineAt(content: string, line: number): string;
8
+ /** Offset of the first character of a 1-based line, or -1 when out of range. */
9
+ export declare function lineStartIndex(content: string, line: number): number;
10
+ export declare function truncate(text: string, max?: number): string;
11
+ /**
12
+ * Makes text safe to print on one terminal line: control and invisible
13
+ * characters become visible escapes such as ​, so hidden payloads show up
14
+ * in reports instead of being smuggled into them.
15
+ */
16
+ export declare function printable(text: string): string;
17
+ /** Zero-width, bidi-control, tag and other format characters that render as nothing. */
18
+ export declare function isInvisible(code: number): boolean;
19
+ export interface RegexHit {
20
+ index: number;
21
+ text: string;
22
+ groups: (string | undefined)[];
23
+ }
24
+ /**
25
+ * Runs a regex over content and returns every hit. The regex is copied with the
26
+ * global flag added, so callers can pass literals without worrying about lastIndex.
27
+ * Stops after `limit` hits so one noisy file cannot flood a report.
28
+ */
29
+ export declare function findAll(regex: RegExp, content: string, limit?: number): RegexHit[];
@@ -0,0 +1,96 @@
1
+ /** Converts a character offset into 1-based line and column numbers. */
2
+ export function lineColAt(content, index) {
3
+ const clamped = Math.max(0, Math.min(index, content.length));
4
+ let line = 1;
5
+ let lastNewline = -1;
6
+ for (let i = content.indexOf('\n'); i !== -1 && i < clamped; i = content.indexOf('\n', i + 1)) {
7
+ line++;
8
+ lastNewline = i;
9
+ }
10
+ return { line, column: clamped - lastNewline };
11
+ }
12
+ /** Returns the text of a 1-based line without its line terminator, or '' when out of range. */
13
+ export function lineAt(content, line) {
14
+ if (line < 1)
15
+ return '';
16
+ let start = 0;
17
+ for (let current = 1; current < line; current++) {
18
+ const next = content.indexOf('\n', start);
19
+ if (next === -1)
20
+ return '';
21
+ start = next + 1;
22
+ }
23
+ const end = content.indexOf('\n', start);
24
+ return content.slice(start, end === -1 ? content.length : end).replace(/\r$/, '');
25
+ }
26
+ /** Offset of the first character of a 1-based line, or -1 when out of range. */
27
+ export function lineStartIndex(content, line) {
28
+ if (line < 1)
29
+ return -1;
30
+ let start = 0;
31
+ for (let current = 1; current < line; current++) {
32
+ const next = content.indexOf('\n', start);
33
+ if (next === -1)
34
+ return -1;
35
+ start = next + 1;
36
+ }
37
+ return start;
38
+ }
39
+ export function truncate(text, max = 160) {
40
+ return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
41
+ }
42
+ /**
43
+ * Makes text safe to print on one terminal line: control and invisible
44
+ * characters become visible escapes such as ​, so hidden payloads show up
45
+ * in reports instead of being smuggled into them.
46
+ */
47
+ export function printable(text) {
48
+ let out = '';
49
+ for (const ch of text) {
50
+ const code = ch.codePointAt(0) ?? 0;
51
+ if (ch === '\t')
52
+ out += ' ';
53
+ else if (isInvisible(code) || code < 0x20 || code === 0x7f) {
54
+ out += code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
55
+ }
56
+ else
57
+ out += ch;
58
+ }
59
+ return out;
60
+ }
61
+ /** Zero-width, bidi-control, tag and other format characters that render as nothing. */
62
+ export function isInvisible(code) {
63
+ return ((code >= 0x200b && code <= 0x200f) ||
64
+ (code >= 0x202a && code <= 0x202e) ||
65
+ (code >= 0x2060 && code <= 0x2064) ||
66
+ (code >= 0x2066 && code <= 0x2069) ||
67
+ code === 0x00ad ||
68
+ code === 0x034f ||
69
+ code === 0x061c ||
70
+ code === 0x115f ||
71
+ code === 0x1160 ||
72
+ code === 0x180e ||
73
+ code === 0x3164 ||
74
+ code === 0xfeff ||
75
+ code === 0xffa0 ||
76
+ (code >= 0xfe00 && code <= 0xfe0f) ||
77
+ (code >= 0xe0000 && code <= 0xe007f) ||
78
+ (code >= 0xe0100 && code <= 0xe01ef));
79
+ }
80
+ /**
81
+ * Runs a regex over content and returns every hit. The regex is copied with the
82
+ * global flag added, so callers can pass literals without worrying about lastIndex.
83
+ * Stops after `limit` hits so one noisy file cannot flood a report.
84
+ */
85
+ export function findAll(regex, content, limit = 50) {
86
+ const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
87
+ const re = new RegExp(regex.source, flags);
88
+ const hits = [];
89
+ let match;
90
+ while (hits.length < limit && (match = re.exec(content)) !== null) {
91
+ hits.push({ index: match.index, text: match[0], groups: match.slice(1) });
92
+ if (match[0].length === 0)
93
+ re.lastIndex++;
94
+ }
95
+ return hits;
96
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * toml-lite: a small, defensive TOML reader for the TOML files toolmoat
3
+ * inspects, mainly Codex's ~/.codex/config.toml and pyproject.toml.
4
+ *
5
+ * Supported: [tables], [[arrays of tables]], dotted and quoted keys, basic and
6
+ * literal strings (single and multi-line, with escapes), integers (decimal,
7
+ * hex, octal, binary), floats, booleans, dates and times (kept as their
8
+ * source text), arrays (multi-line, nested, trailing commas) and inline
9
+ * tables. A statement it cannot read is skipped and listed in `errors`, so a
10
+ * malformed or hostile file never throws. It runs in linear time with bounded
11
+ * nesting because scanned files are attacker-controlled.
12
+ *
13
+ * Not a validator: duplicate keys overwrite, and integers beyond 2^53 lose
14
+ * precision.
15
+ */
16
+ export interface TomlError {
17
+ /** 1-based line where parsing failed. */
18
+ line: number;
19
+ message: string;
20
+ }
21
+ export interface TomlDocument {
22
+ /** Parsed tables and values. Keys such as "__proto__" are stored as ordinary own properties. */
23
+ data: Record<string, unknown>;
24
+ /**
25
+ * 1-based line of every key, table header and array element, keyed by its
26
+ * full path as built by joinKeyPath, for example 'mcp_servers.github.args.1'.
27
+ */
28
+ lines: Map<string, number>;
29
+ errors: TomlError[];
30
+ }
31
+ type Segment = string | number;
32
+ /**
33
+ * Joins a key path the way `TomlDocument.lines` is keyed: bare segments as
34
+ * they are, other segments JSON-quoted, array indexes as numbers.
35
+ * joinKeyPath(['mcp_servers', 'my.server', 'args', 0]) === 'mcp_servers."my.server".args.0'
36
+ */
37
+ export declare function joinKeyPath(segments: readonly Segment[]): string;
38
+ /** Parses TOML text. Never throws; unreadable statements are skipped and reported in `errors`. */
39
+ export declare function parseToml(text: string): TomlDocument;
40
+ export {};