sentinelayer-cli 0.39.2 → 0.40.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/package.json +1 -1
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +92 -31
- package/src/legacy-cli.js +2 -2
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/checkpoints.js +194 -0
|
@@ -1,181 +1,36 @@
|
|
|
1
|
-
// Shared helpers for
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
return "P2";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function createFinding({
|
|
41
|
-
severity,
|
|
42
|
-
kind,
|
|
43
|
-
file,
|
|
44
|
-
line = 0,
|
|
45
|
-
evidence = "",
|
|
46
|
-
rootCause = "",
|
|
47
|
-
recommendedFix = "",
|
|
48
|
-
confidence = null,
|
|
49
|
-
tool = "",
|
|
50
|
-
persona = "reliability",
|
|
51
|
-
} = {}) {
|
|
52
|
-
return {
|
|
53
|
-
persona,
|
|
54
|
-
tool: String(tool || "").trim(),
|
|
55
|
-
kind: String(kind || "").trim() || "reliability",
|
|
56
|
-
severity: normalizeSeverity(severity),
|
|
57
|
-
file: toPosix(file || ""),
|
|
58
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
59
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
60
|
-
rootCause: String(rootCause || "").trim(),
|
|
61
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
62
|
-
confidence:
|
|
63
|
-
confidence === null || confidence === undefined
|
|
64
|
-
? null
|
|
65
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function readIgnorePatterns(filePath) {
|
|
70
|
-
try {
|
|
71
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
72
|
-
return String(raw || "")
|
|
73
|
-
.split(/\r?\n/)
|
|
74
|
-
.map((line) => line.trim())
|
|
75
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
76
|
-
} catch (err) {
|
|
77
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
1
|
+
// Shared helpers for Rosa's (reliability) domain tools (#A18).
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_IGNORED_DIRS,
|
|
5
|
+
MAX_FILE_SIZE_BYTES,
|
|
6
|
+
SEVERITIES,
|
|
7
|
+
createFinding as createDomainFinding,
|
|
8
|
+
findLineMatches,
|
|
9
|
+
getLineContent,
|
|
10
|
+
normalizeSeverity,
|
|
11
|
+
toPosix,
|
|
12
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
13
|
+
} from "../../shared-tools/domain-base.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PERSONA = "reliability";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_IGNORED_DIRS,
|
|
19
|
+
MAX_FILE_SIZE_BYTES,
|
|
20
|
+
SEVERITIES,
|
|
21
|
+
findLineMatches,
|
|
22
|
+
getLineContent,
|
|
23
|
+
normalizeSeverity,
|
|
24
|
+
toPosix,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createFinding(options = {}) {
|
|
28
|
+
return createDomainFinding(options, {
|
|
29
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
30
|
+
defaultKind: DEFAULT_PERSONA,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function* walkRepoFiles(options = {}) {
|
|
35
|
+
yield* walkDomainRepoFiles(options);
|
|
82
36
|
}
|
|
83
|
-
|
|
84
|
-
async function createIgnoreMatcher(rootPath) {
|
|
85
|
-
const matcher = ignore();
|
|
86
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
87
|
-
const sentinel = await readIgnorePatterns(
|
|
88
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
89
|
-
);
|
|
90
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
91
|
-
return (relativePath, isDirectory) => {
|
|
92
|
-
const normalized = toPosix(relativePath);
|
|
93
|
-
if (!normalized) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
97
|
-
return matcher.ignores(candidate);
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function* walkRepoFiles({
|
|
102
|
-
rootPath = process.cwd(),
|
|
103
|
-
extensions = new Set(),
|
|
104
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
105
|
-
} = {}) {
|
|
106
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
107
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
108
|
-
const wantedExtensions =
|
|
109
|
-
extensions instanceof Set
|
|
110
|
-
? extensions
|
|
111
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
112
|
-
const stack = [resolvedRoot];
|
|
113
|
-
while (stack.length > 0) {
|
|
114
|
-
const current = stack.pop();
|
|
115
|
-
let entries = [];
|
|
116
|
-
try {
|
|
117
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
118
|
-
} catch {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
for (const entry of entries) {
|
|
122
|
-
const fullPath = path.join(current, entry.name);
|
|
123
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
124
|
-
if (entry.isDirectory()) {
|
|
125
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
stack.push(fullPath);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (!entry.isFile()) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
141
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
let stat = null;
|
|
145
|
-
try {
|
|
146
|
-
stat = await fsp.stat(fullPath);
|
|
147
|
-
} catch {
|
|
148
|
-
stat = null;
|
|
149
|
-
}
|
|
150
|
-
if (!stat || stat.size > maxFileSize) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
yield { fullPath, relativePath };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function findLineMatches(content, pattern) {
|
|
159
|
-
const text = String(content || "");
|
|
160
|
-
if (!pattern) {
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
const global = new RegExp(
|
|
164
|
-
pattern.source,
|
|
165
|
-
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
166
|
-
);
|
|
167
|
-
const matches = [];
|
|
168
|
-
let match;
|
|
169
|
-
while ((match = global.exec(text)) !== null) {
|
|
170
|
-
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
171
|
-
matches.push({ index: match.index, line: lineIndex, match: match[0] });
|
|
172
|
-
}
|
|
173
|
-
return matches;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function getLineContent(content, line) {
|
|
177
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
178
|
-
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|
|
@@ -1,190 +1,36 @@
|
|
|
1
1
|
// Shared helpers for Nina's (security) domain tools (#A13).
|
|
2
|
-
//
|
|
3
|
-
// Every security tool returns the same Finding shape so the orchestrator can
|
|
4
|
-
// bin / merge results without case analysis. We also centralize the file
|
|
5
|
-
// walker and scope filter here so no tool re-invents the ignore logic.
|
|
6
2
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return "P2";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_IGNORED_DIRS,
|
|
5
|
+
MAX_FILE_SIZE_BYTES,
|
|
6
|
+
SEVERITIES,
|
|
7
|
+
createFinding as createDomainFinding,
|
|
8
|
+
evidenceAroundMatch,
|
|
9
|
+
lineNumberOf,
|
|
10
|
+
normalizeSeverity,
|
|
11
|
+
toPosix,
|
|
12
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
13
|
+
} from "../../shared-tools/domain-base.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PERSONA = "security";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_IGNORED_DIRS,
|
|
19
|
+
MAX_FILE_SIZE_BYTES,
|
|
20
|
+
SEVERITIES,
|
|
21
|
+
evidenceAroundMatch,
|
|
22
|
+
lineNumberOf,
|
|
23
|
+
normalizeSeverity,
|
|
24
|
+
toPosix,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createFinding(options = {}) {
|
|
28
|
+
return createDomainFinding(options, {
|
|
29
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
30
|
+
defaultKind: DEFAULT_PERSONA,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function* walkRepoFiles(options = {}) {
|
|
35
|
+
yield* walkDomainRepoFiles(options);
|
|
42
36
|
}
|
|
43
|
-
|
|
44
|
-
// Canonical Finding shape — every security tool returns objects matching
|
|
45
|
-
// this schema.
|
|
46
|
-
export function createFinding({
|
|
47
|
-
severity,
|
|
48
|
-
kind,
|
|
49
|
-
file,
|
|
50
|
-
line = 0,
|
|
51
|
-
evidence = "",
|
|
52
|
-
rootCause = "",
|
|
53
|
-
recommendedFix = "",
|
|
54
|
-
confidence = null,
|
|
55
|
-
tool = "",
|
|
56
|
-
persona = "security",
|
|
57
|
-
} = {}) {
|
|
58
|
-
return {
|
|
59
|
-
persona,
|
|
60
|
-
tool: String(tool || "").trim(),
|
|
61
|
-
kind: String(kind || "").trim() || "security",
|
|
62
|
-
severity: normalizeSeverity(severity),
|
|
63
|
-
file: toPosix(file || ""),
|
|
64
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
65
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
66
|
-
rootCause: String(rootCause || "").trim(),
|
|
67
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
68
|
-
confidence:
|
|
69
|
-
confidence === null || confidence === undefined
|
|
70
|
-
? null
|
|
71
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function readIgnorePatterns(filePath) {
|
|
76
|
-
try {
|
|
77
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
78
|
-
return String(raw || "")
|
|
79
|
-
.split(/\r?\n/)
|
|
80
|
-
.map((line) => line.trim())
|
|
81
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function createIgnoreMatcher(rootPath) {
|
|
91
|
-
const matcher = ignore();
|
|
92
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
93
|
-
const sentinel = await readIgnorePatterns(
|
|
94
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
95
|
-
);
|
|
96
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
97
|
-
return (relativePath, isDirectory) => {
|
|
98
|
-
const normalized = toPosix(relativePath);
|
|
99
|
-
if (!normalized) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
103
|
-
return matcher.ignores(candidate);
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Walk the target repo, yielding files whose extension is in `extensions`
|
|
108
|
-
// (or all files when `extensions` is empty). Respects .gitignore +
|
|
109
|
-
// .sentinelayerignore + DEFAULT_IGNORED_DIRS.
|
|
110
|
-
export async function* walkRepoFiles({
|
|
111
|
-
rootPath = process.cwd(),
|
|
112
|
-
extensions = new Set(),
|
|
113
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
114
|
-
} = {}) {
|
|
115
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
116
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
117
|
-
const wantedExtensions =
|
|
118
|
-
extensions instanceof Set
|
|
119
|
-
? extensions
|
|
120
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
121
|
-
const stack = [resolvedRoot];
|
|
122
|
-
|
|
123
|
-
while (stack.length > 0) {
|
|
124
|
-
const current = stack.pop();
|
|
125
|
-
let entries = [];
|
|
126
|
-
try {
|
|
127
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
128
|
-
} catch {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
for (const entry of entries) {
|
|
132
|
-
const fullPath = path.join(current, entry.name);
|
|
133
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
134
|
-
if (entry.isDirectory()) {
|
|
135
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
stack.push(fullPath);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (!entry.isFile()) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
151
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
let stat = null;
|
|
155
|
-
try {
|
|
156
|
-
stat = await fsp.stat(fullPath);
|
|
157
|
-
} catch {
|
|
158
|
-
stat = null;
|
|
159
|
-
}
|
|
160
|
-
if (!stat || stat.size > maxFileSize) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
yield { fullPath, relativePath };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Find line number of the first occurrence of a pattern in `content`.
|
|
169
|
-
// Returns 0 when not found.
|
|
170
|
-
export function lineNumberOf(content, pattern) {
|
|
171
|
-
const text = String(content || "");
|
|
172
|
-
if (!pattern) {
|
|
173
|
-
return 0;
|
|
174
|
-
}
|
|
175
|
-
const idx = text.search(pattern);
|
|
176
|
-
if (idx < 0) {
|
|
177
|
-
return 0;
|
|
178
|
-
}
|
|
179
|
-
const before = text.slice(0, idx);
|
|
180
|
-
return before.split(/\r?\n/).length;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Capture one matching line (trimmed) for evidence.
|
|
184
|
-
export function evidenceAroundMatch(content, line) {
|
|
185
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
186
|
-
const idx = Math.max(0, (Number(line) || 1) - 1);
|
|
187
|
-
return (lines[idx] || "").trim();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Shared primitives for deterministic persona domain tools.
|
|
2
|
+
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
import ignore from "ignore";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_IGNORED_DIRS = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
"node_modules",
|
|
12
|
+
".venv",
|
|
13
|
+
".next",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
"coverage",
|
|
17
|
+
".sentinelayer",
|
|
18
|
+
".sentinel",
|
|
19
|
+
".turbo",
|
|
20
|
+
".idea",
|
|
21
|
+
".vscode",
|
|
22
|
+
"__pycache__",
|
|
23
|
+
".cache",
|
|
24
|
+
]);
|
|
25
|
+
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
26
|
+
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
27
|
+
|
|
28
|
+
export function toPosix(value) {
|
|
29
|
+
return String(value || "").replace(/\\/g, "/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeSeverity(value, fallback = "P2") {
|
|
33
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
34
|
+
if (SEVERITIES.includes(normalized)) {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
return SEVERITIES.includes(fallback) ? fallback : "P2";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createFinding(
|
|
41
|
+
{
|
|
42
|
+
severity,
|
|
43
|
+
kind,
|
|
44
|
+
file,
|
|
45
|
+
line = 0,
|
|
46
|
+
evidence = "",
|
|
47
|
+
rootCause = "",
|
|
48
|
+
recommendedFix = "",
|
|
49
|
+
confidence = null,
|
|
50
|
+
tool = "",
|
|
51
|
+
persona,
|
|
52
|
+
} = {},
|
|
53
|
+
{ defaultPersona = "", defaultKind = defaultPersona, severityFallback = "P2" } = {}
|
|
54
|
+
) {
|
|
55
|
+
const findingPersona =
|
|
56
|
+
String(persona || defaultPersona || "").trim() || "domain";
|
|
57
|
+
return {
|
|
58
|
+
persona: findingPersona,
|
|
59
|
+
tool: String(tool || "").trim(),
|
|
60
|
+
kind: String(kind || "").trim() || String(defaultKind || findingPersona).trim(),
|
|
61
|
+
severity: normalizeSeverity(severity, severityFallback),
|
|
62
|
+
file: toPosix(file || ""),
|
|
63
|
+
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
64
|
+
evidence: String(evidence || "").trim().slice(0, 400),
|
|
65
|
+
rootCause: String(rootCause || "").trim(),
|
|
66
|
+
recommendedFix: String(recommendedFix || "").trim(),
|
|
67
|
+
confidence:
|
|
68
|
+
confidence === null || confidence === undefined
|
|
69
|
+
? null
|
|
70
|
+
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readIgnorePatterns(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
77
|
+
return String(raw || "")
|
|
78
|
+
.split(/\r?\n/)
|
|
79
|
+
.map((line) => line.trim())
|
|
80
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function createIgnoreMatcher(rootPath) {
|
|
90
|
+
const matcher = ignore();
|
|
91
|
+
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
92
|
+
const sentinel = await readIgnorePatterns(
|
|
93
|
+
path.join(rootPath, ".sentinelayerignore")
|
|
94
|
+
);
|
|
95
|
+
matcher.add([...gitignore, ...sentinel]);
|
|
96
|
+
return (relativePath, isDirectory) => {
|
|
97
|
+
const normalized = toPosix(relativePath);
|
|
98
|
+
if (!normalized) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
102
|
+
return matcher.ignores(candidate);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function* walkRepoFiles(
|
|
107
|
+
{
|
|
108
|
+
rootPath = process.cwd(),
|
|
109
|
+
extensions = new Set(),
|
|
110
|
+
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
111
|
+
} = {},
|
|
112
|
+
{ entryMetadata = "none" } = {}
|
|
113
|
+
) {
|
|
114
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
115
|
+
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
116
|
+
const wantedExtensions =
|
|
117
|
+
extensions instanceof Set
|
|
118
|
+
? extensions
|
|
119
|
+
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
120
|
+
const stack = [resolvedRoot];
|
|
121
|
+
while (stack.length > 0) {
|
|
122
|
+
const current = stack.pop();
|
|
123
|
+
let entries = [];
|
|
124
|
+
try {
|
|
125
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const fullPath = path.join(current, entry.name);
|
|
131
|
+
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (ignoreMatcher(relativePath, true)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
stack.push(fullPath);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!entry.isFile()) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ignoreMatcher(relativePath, false)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
149
|
+
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
let stat = null;
|
|
153
|
+
try {
|
|
154
|
+
stat = await fsp.stat(fullPath);
|
|
155
|
+
} catch {
|
|
156
|
+
stat = null;
|
|
157
|
+
}
|
|
158
|
+
if (!stat || stat.size > maxFileSize) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (entryMetadata === "stat") {
|
|
162
|
+
yield { fullPath, relativePath, stat };
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (entryMetadata === "sizeBytes") {
|
|
166
|
+
yield { fullPath, relativePath, sizeBytes: stat.size };
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
yield { fullPath, relativePath };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function findLineMatches(content, pattern) {
|
|
175
|
+
const text = String(content || "");
|
|
176
|
+
if (!pattern) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
const global = new RegExp(
|
|
180
|
+
pattern.source,
|
|
181
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
182
|
+
);
|
|
183
|
+
const matches = [];
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = global.exec(text)) !== null) {
|
|
186
|
+
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
187
|
+
matches.push({
|
|
188
|
+
index: match.index,
|
|
189
|
+
line: lineIndex,
|
|
190
|
+
match: match[0],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return matches;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getLineContent(content, line) {
|
|
197
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
198
|
+
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function lineNumberOf(content, pattern) {
|
|
202
|
+
const text = String(content || "");
|
|
203
|
+
if (!pattern) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
const idx = text.search(pattern);
|
|
207
|
+
if (idx < 0) {
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
const before = text.slice(0, idx);
|
|
211
|
+
return before.split(/\r?\n/).length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function evidenceAroundMatch(content, line) {
|
|
215
|
+
return getLineContent(content, line);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function* iterateFiles(resolvedRoot, files, extensions) {
|
|
219
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
const trimmed = String(file || "").trim();
|
|
222
|
+
if (!trimmed) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const fullPath = path.isAbsolute(trimmed)
|
|
226
|
+
? trimmed
|
|
227
|
+
: path.join(resolvedRoot, trimmed);
|
|
228
|
+
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
229
|
+
let stat = null;
|
|
230
|
+
try {
|
|
231
|
+
stat = await fsp.stat(fullPath);
|
|
232
|
+
} catch {
|
|
233
|
+
stat = null;
|
|
234
|
+
}
|
|
235
|
+
yield { fullPath, relativePath, sizeBytes: stat?.size || 0 };
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
yield* walkRepoFiles(
|
|
241
|
+
{ rootPath: resolvedRoot, extensions },
|
|
242
|
+
{ entryMetadata: "sizeBytes" }
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|