sapper-ai 0.7.0 → 0.8.1
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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +280 -5
- package/dist/guard/ScanCache.d.ts +42 -0
- package/dist/guard/ScanCache.d.ts.map +1 -0
- package/dist/guard/ScanCache.js +261 -0
- package/dist/guard/WarningStore.d.ts +33 -0
- package/dist/guard/WarningStore.d.ts.map +1 -0
- package/dist/guard/WarningStore.js +305 -0
- package/dist/guard/getDefaultPolicy.d.ts +3 -0
- package/dist/guard/getDefaultPolicy.d.ts.map +1 -0
- package/dist/guard/getDefaultPolicy.js +23 -0
- package/dist/guard/hooks/guardCheck.d.ts +12 -0
- package/dist/guard/hooks/guardCheck.d.ts.map +1 -0
- package/dist/guard/hooks/guardCheck.js +137 -0
- package/dist/guard/hooks/guardScan.d.ts +27 -0
- package/dist/guard/hooks/guardScan.d.ts.map +1 -0
- package/dist/guard/hooks/guardScan.js +242 -0
- package/dist/guard/scanSingleSkill.d.ts +11 -0
- package/dist/guard/scanSingleSkill.d.ts.map +1 -0
- package/dist/guard/scanSingleSkill.js +82 -0
- package/dist/guard/setup.d.ts +44 -0
- package/dist/guard/setup.d.ts.map +1 -0
- package/dist/guard/setup.js +296 -0
- package/dist/guard/types.d.ts +43 -0
- package/dist/guard/types.d.ts.map +1 -0
- package/dist/guard/types.js +2 -0
- package/dist/harden.d.ts.map +1 -1
- package/dist/harden.js +2 -6
- package/dist/postinstall.d.ts.map +1 -1
- package/dist/postinstall.js +40 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +7 -2
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +7 -1
- package/dist/utils/interactive.d.ts +15 -0
- package/dist/utils/interactive.d.ts.map +1 -0
- package/dist/utils/interactive.js +29 -0
- package/package.json +3 -3
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WarningStore = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const fs_1 = require("../utils/fs");
|
|
8
|
+
const WARNING_STORE_VERSION = 1;
|
|
9
|
+
const WARNING_DIR = '.sapper-ai';
|
|
10
|
+
const WARNING_FILE = 'warnings.json';
|
|
11
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
12
|
+
function cloneWarning(warning) {
|
|
13
|
+
return {
|
|
14
|
+
skillName: warning.skillName,
|
|
15
|
+
skillPath: warning.skillPath,
|
|
16
|
+
contentHash: warning.contentHash,
|
|
17
|
+
risk: warning.risk,
|
|
18
|
+
reasons: [...warning.reasons],
|
|
19
|
+
detectedAt: warning.detectedAt,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function cloneDismissed(dismissed) {
|
|
23
|
+
return {
|
|
24
|
+
skillName: dismissed.skillName,
|
|
25
|
+
contentHash: dismissed.contentHash,
|
|
26
|
+
dismissedAt: dismissed.dismissedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function normalizeWarning(value) {
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const warning = value;
|
|
34
|
+
if (typeof warning.skillName !== 'string' || warning.skillName.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (typeof warning.skillPath !== 'string' || warning.skillPath.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof warning.contentHash !== 'string' || warning.contentHash.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (typeof warning.risk !== 'number' || !Number.isFinite(warning.risk)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(warning.reasons) || warning.reasons.some((reason) => typeof reason !== 'string')) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (typeof warning.detectedAt !== 'string' || warning.detectedAt.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
skillName: warning.skillName,
|
|
54
|
+
skillPath: warning.skillPath,
|
|
55
|
+
contentHash: warning.contentHash,
|
|
56
|
+
risk: warning.risk,
|
|
57
|
+
reasons: [...warning.reasons],
|
|
58
|
+
detectedAt: warning.detectedAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function normalizeDismissed(value) {
|
|
62
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const dismissed = value;
|
|
66
|
+
if (typeof dismissed.skillName !== 'string' || dismissed.skillName.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (dismissed.contentHash !== undefined && typeof dismissed.contentHash !== 'string') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (typeof dismissed.dismissedAt !== 'string' || dismissed.dismissedAt.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
skillName: dismissed.skillName,
|
|
77
|
+
contentHash: dismissed.contentHash,
|
|
78
|
+
dismissedAt: dismissed.dismissedAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function normalizeWarningList(value) {
|
|
82
|
+
if (!Array.isArray(value)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const warnings = [];
|
|
86
|
+
for (const warning of value) {
|
|
87
|
+
const normalized = normalizeWarning(warning);
|
|
88
|
+
if (!normalized) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
warnings.push(normalized);
|
|
92
|
+
}
|
|
93
|
+
return warnings;
|
|
94
|
+
}
|
|
95
|
+
function normalizeDismissedList(value) {
|
|
96
|
+
if (!Array.isArray(value)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const dismissedList = [];
|
|
100
|
+
for (const dismissed of value) {
|
|
101
|
+
const normalized = normalizeDismissed(dismissed);
|
|
102
|
+
if (!normalized) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
dismissedList.push(normalized);
|
|
106
|
+
}
|
|
107
|
+
return dismissedList;
|
|
108
|
+
}
|
|
109
|
+
function isSameWarning(left, right) {
|
|
110
|
+
return (left.skillName === right.skillName &&
|
|
111
|
+
left.skillPath === right.skillPath &&
|
|
112
|
+
left.contentHash === right.contentHash);
|
|
113
|
+
}
|
|
114
|
+
function hasDismissedMatch(dismissed, skillName, contentHash) {
|
|
115
|
+
if (dismissed.skillName !== skillName) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (!dismissed.contentHash) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return dismissed.contentHash === contentHash;
|
|
122
|
+
}
|
|
123
|
+
class WarningStore {
|
|
124
|
+
constructor(options = {}) {
|
|
125
|
+
this.state = null;
|
|
126
|
+
this.filePath = options.filePath ?? (0, node_path_1.join)(options.homeDir ?? (0, node_os_1.homedir)(), WARNING_DIR, WARNING_FILE);
|
|
127
|
+
this.now = options.now ?? Date.now;
|
|
128
|
+
this.readFileFn = options.readFileFn ?? promises_1.readFile;
|
|
129
|
+
this.writeFileFn =
|
|
130
|
+
options.writeFileFn ??
|
|
131
|
+
((filePath, content) => (0, fs_1.atomicWriteFile)(filePath, content, { mode: PRIVATE_FILE_MODE }));
|
|
132
|
+
}
|
|
133
|
+
async addPending(warning) {
|
|
134
|
+
const state = await this.loadState();
|
|
135
|
+
const normalized = this.normalizeInputWarning(warning);
|
|
136
|
+
if (this.isDismissedInState(state, normalized.skillName, normalized.contentHash)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const existingIndex = state.pending.findIndex((entry) => entry.skillName === normalized.skillName && entry.skillPath === normalized.skillPath);
|
|
140
|
+
if (existingIndex >= 0) {
|
|
141
|
+
state.pending[existingIndex] = normalized;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
state.pending.push(normalized);
|
|
145
|
+
}
|
|
146
|
+
await this.persist(state);
|
|
147
|
+
}
|
|
148
|
+
async getPending() {
|
|
149
|
+
const state = await this.loadState();
|
|
150
|
+
return state.pending.map(cloneWarning);
|
|
151
|
+
}
|
|
152
|
+
async getAcknowledged() {
|
|
153
|
+
const state = await this.loadState();
|
|
154
|
+
return state.acknowledged.map(cloneWarning);
|
|
155
|
+
}
|
|
156
|
+
async getDismissed() {
|
|
157
|
+
const state = await this.loadState();
|
|
158
|
+
return state.dismissed.map(cloneDismissed);
|
|
159
|
+
}
|
|
160
|
+
async isDismissed(skillName, contentHash) {
|
|
161
|
+
const state = await this.loadState();
|
|
162
|
+
return this.isDismissedInState(state, skillName, contentHash);
|
|
163
|
+
}
|
|
164
|
+
async acknowledge(skillName, skillPath) {
|
|
165
|
+
const state = await this.loadState();
|
|
166
|
+
const matched = state.pending.filter((entry) => skillPath ? entry.skillName === skillName && entry.skillPath === skillPath : entry.skillName === skillName);
|
|
167
|
+
if (matched.length === 0) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
state.pending = state.pending.filter((entry) => skillPath ? !(entry.skillName === skillName && entry.skillPath === skillPath) : entry.skillName !== skillName);
|
|
171
|
+
for (const warning of matched) {
|
|
172
|
+
const alreadyAcknowledged = state.acknowledged.some((entry) => isSameWarning(entry, warning));
|
|
173
|
+
if (!alreadyAcknowledged) {
|
|
174
|
+
state.acknowledged.push(cloneWarning(warning));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await this.persist(state);
|
|
178
|
+
return matched.length;
|
|
179
|
+
}
|
|
180
|
+
async acknowledgeAll(warnings) {
|
|
181
|
+
if (warnings.length === 0) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
const state = await this.loadState();
|
|
185
|
+
const targetKeys = new Set(warnings.map((warning) => `${warning.skillName}\u0000${warning.skillPath}`));
|
|
186
|
+
let movedCount = 0;
|
|
187
|
+
const nextPending = [];
|
|
188
|
+
for (const warning of state.pending) {
|
|
189
|
+
const key = `${warning.skillName}\u0000${warning.skillPath}`;
|
|
190
|
+
if (!targetKeys.has(key)) {
|
|
191
|
+
nextPending.push(warning);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
movedCount += 1;
|
|
195
|
+
const alreadyAcknowledged = state.acknowledged.some((entry) => isSameWarning(entry, warning));
|
|
196
|
+
if (!alreadyAcknowledged) {
|
|
197
|
+
state.acknowledged.push(cloneWarning(warning));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (movedCount === 0) {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
state.pending = nextPending;
|
|
204
|
+
await this.persist(state);
|
|
205
|
+
return movedCount;
|
|
206
|
+
}
|
|
207
|
+
async dismiss(skillName) {
|
|
208
|
+
const state = await this.loadState();
|
|
209
|
+
const matchedWarnings = [
|
|
210
|
+
...state.pending.filter((entry) => entry.skillName === skillName),
|
|
211
|
+
...state.acknowledged.filter((entry) => entry.skillName === skillName),
|
|
212
|
+
];
|
|
213
|
+
state.pending = state.pending.filter((entry) => entry.skillName !== skillName);
|
|
214
|
+
state.acknowledged = state.acknowledged.filter((entry) => entry.skillName !== skillName);
|
|
215
|
+
state.dismissed = state.dismissed.filter((entry) => entry.skillName !== skillName);
|
|
216
|
+
state.dismissed.push({
|
|
217
|
+
skillName,
|
|
218
|
+
dismissedAt: new Date(this.now()).toISOString(),
|
|
219
|
+
});
|
|
220
|
+
await this.persist(state);
|
|
221
|
+
return matchedWarnings.length;
|
|
222
|
+
}
|
|
223
|
+
async clearPending() {
|
|
224
|
+
const state = await this.loadState();
|
|
225
|
+
state.pending = [];
|
|
226
|
+
await this.persist(state);
|
|
227
|
+
}
|
|
228
|
+
async replacePending(warnings) {
|
|
229
|
+
const state = await this.loadState();
|
|
230
|
+
const nextPending = [];
|
|
231
|
+
for (const warning of warnings) {
|
|
232
|
+
const normalized = this.normalizeInputWarning(warning);
|
|
233
|
+
if (this.isDismissedInState(state, normalized.skillName, normalized.contentHash)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const duplicate = nextPending.some((entry) => entry.skillName === normalized.skillName &&
|
|
237
|
+
entry.skillPath === normalized.skillPath &&
|
|
238
|
+
entry.contentHash === normalized.contentHash);
|
|
239
|
+
if (!duplicate) {
|
|
240
|
+
nextPending.push(normalized);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
state.pending = nextPending;
|
|
244
|
+
await this.persist(state);
|
|
245
|
+
}
|
|
246
|
+
normalizeInputWarning(warning) {
|
|
247
|
+
const normalized = normalizeWarning(warning);
|
|
248
|
+
if (!normalized) {
|
|
249
|
+
throw new Error('Invalid warning payload');
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
isDismissedInState(state, skillName, contentHash) {
|
|
254
|
+
return state.dismissed.some((entry) => hasDismissedMatch(entry, skillName, contentHash));
|
|
255
|
+
}
|
|
256
|
+
async loadState() {
|
|
257
|
+
if (this.state) {
|
|
258
|
+
return this.state;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const raw = await this.readFileFn(this.filePath, 'utf8');
|
|
262
|
+
const parsed = JSON.parse(raw);
|
|
263
|
+
const normalized = this.normalizeState(parsed);
|
|
264
|
+
this.state = normalized;
|
|
265
|
+
return normalized;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
const empty = this.createEmptyState();
|
|
269
|
+
this.state = empty;
|
|
270
|
+
return empty;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
normalizeState(raw) {
|
|
274
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
275
|
+
return this.createEmptyState();
|
|
276
|
+
}
|
|
277
|
+
const record = raw;
|
|
278
|
+
const version = typeof record.version === 'number' ? record.version : WARNING_STORE_VERSION;
|
|
279
|
+
return {
|
|
280
|
+
version,
|
|
281
|
+
pending: normalizeWarningList(record.pending),
|
|
282
|
+
acknowledged: normalizeWarningList(record.acknowledged),
|
|
283
|
+
dismissed: normalizeDismissedList(record.dismissed),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
createEmptyState() {
|
|
287
|
+
return {
|
|
288
|
+
version: WARNING_STORE_VERSION,
|
|
289
|
+
pending: [],
|
|
290
|
+
acknowledged: [],
|
|
291
|
+
dismissed: [],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async persist(state) {
|
|
295
|
+
state.version = WARNING_STORE_VERSION;
|
|
296
|
+
const payload = JSON.stringify({
|
|
297
|
+
version: state.version,
|
|
298
|
+
pending: state.pending,
|
|
299
|
+
acknowledged: state.acknowledged,
|
|
300
|
+
dismissed: state.dismissed,
|
|
301
|
+
}, null, 2);
|
|
302
|
+
await this.writeFileFn(this.filePath, `${payload}\n`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
exports.WarningStore = WarningStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getDefaultPolicy.d.ts","sourceRoot":"","sources":["../../src/guard/getDefaultPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAa9C,wBAAgB,gBAAgB,IAAI,MAAM,CASzC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDefaultPolicy = getDefaultPolicy;
|
|
4
|
+
const DEFAULT_POLICY = {
|
|
5
|
+
mode: 'enforce',
|
|
6
|
+
defaultAction: 'allow',
|
|
7
|
+
failOpen: true,
|
|
8
|
+
detectors: ['rules'],
|
|
9
|
+
thresholds: {
|
|
10
|
+
riskThreshold: 0.7,
|
|
11
|
+
blockMinConfidence: 0.5,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
function getDefaultPolicy() {
|
|
15
|
+
return {
|
|
16
|
+
...DEFAULT_POLICY,
|
|
17
|
+
detectors: [...(DEFAULT_POLICY.detectors ?? ['rules'])],
|
|
18
|
+
thresholds: {
|
|
19
|
+
riskThreshold: DEFAULT_POLICY.thresholds?.riskThreshold ?? 0.7,
|
|
20
|
+
blockMinConfidence: DEFAULT_POLICY.thresholds?.blockMinConfidence ?? 0.5,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { WarningStore } from '../WarningStore';
|
|
2
|
+
import type { GuardHookOutput, OutputWriter, SingleSkillScanResult } from '../types';
|
|
3
|
+
export interface GuardCheckOptions {
|
|
4
|
+
warningStore?: WarningStore;
|
|
5
|
+
scanSkillFn?: (filePath: string) => Promise<SingleSkillScanResult>;
|
|
6
|
+
stdout?: OutputWriter;
|
|
7
|
+
stderr?: OutputWriter;
|
|
8
|
+
readFileFn?: (filePath: string, encoding: BufferEncoding) => Promise<string>;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare function guardCheck(options?: GuardCheckOptions): Promise<GuardHookOutput>;
|
|
12
|
+
//# sourceMappingURL=guardCheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guardCheck.d.ts","sourceRoot":"","sources":["../../../src/guard/hooks/guardCheck.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,qBAAqB,EAAgB,MAAM,UAAU,CAAA;AAElG,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAClE,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAoCD,wBAAsB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4H1F"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.guardCheck = guardCheck;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const WarningStore_1 = require("../WarningStore");
|
|
7
|
+
const scanSingleSkill_1 = require("../scanSingleSkill");
|
|
8
|
+
function sha256(content) {
|
|
9
|
+
return (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function writeJson(writer, payload) {
|
|
12
|
+
writer.write(`${JSON.stringify(payload)}\n`);
|
|
13
|
+
}
|
|
14
|
+
function writeError(stderr, message) {
|
|
15
|
+
stderr.write(`[sapper-ai] ${message}\n`);
|
|
16
|
+
}
|
|
17
|
+
function toWarningMessage(warnings) {
|
|
18
|
+
const lines = ['⚠ SapperAI: suspicious skills detected'];
|
|
19
|
+
for (const warning of warnings) {
|
|
20
|
+
lines.push(`- ${warning.skillName} (risk: ${warning.risk.toFixed(2)})`);
|
|
21
|
+
for (const reason of warning.reasons.slice(0, 3)) {
|
|
22
|
+
lines.push(` -> ${reason}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push(` path: ${warning.skillPath}`);
|
|
25
|
+
}
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
}
|
|
28
|
+
async function guardCheck(options = {}) {
|
|
29
|
+
const stdout = options.stdout ?? process.stdout;
|
|
30
|
+
const stderr = options.stderr ?? process.stderr;
|
|
31
|
+
const warningStore = options.warningStore ?? new WarningStore_1.WarningStore();
|
|
32
|
+
const scanSkill = options.scanSkillFn ?? ((path) => (0, scanSingleSkill_1.scanSingleSkill)(path));
|
|
33
|
+
const read = options.readFileFn ?? promises_1.readFile;
|
|
34
|
+
const now = options.now ?? Date.now;
|
|
35
|
+
const summary = {
|
|
36
|
+
pending: 0,
|
|
37
|
+
delivered: 0,
|
|
38
|
+
acknowledged: 0,
|
|
39
|
+
removed: 0,
|
|
40
|
+
errors: 0,
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
const pendingWarnings = await warningStore.getPending();
|
|
44
|
+
summary.pending = pendingWarnings.length;
|
|
45
|
+
if (pendingWarnings.length === 0) {
|
|
46
|
+
const payload = {
|
|
47
|
+
suppressPrompt: false,
|
|
48
|
+
message: '',
|
|
49
|
+
warnings: [],
|
|
50
|
+
summary: { ...summary },
|
|
51
|
+
};
|
|
52
|
+
writeJson(stdout, payload);
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
const nextPending = [];
|
|
56
|
+
const deliverWarnings = [];
|
|
57
|
+
for (const warning of pendingWarnings) {
|
|
58
|
+
if (await warningStore.isDismissed(warning.skillName, warning.contentHash)) {
|
|
59
|
+
summary.removed += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const content = await read(warning.skillPath, 'utf8');
|
|
64
|
+
const currentHash = sha256(content);
|
|
65
|
+
if (currentHash === warning.contentHash) {
|
|
66
|
+
deliverWarnings.push(warning);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const rescanned = await scanSkill(warning.skillPath);
|
|
70
|
+
const postScanContent = await read(warning.skillPath, 'utf8');
|
|
71
|
+
const postScanHash = sha256(postScanContent);
|
|
72
|
+
if (postScanHash !== rescanned.contentHash) {
|
|
73
|
+
deliverWarnings.push({
|
|
74
|
+
...warning,
|
|
75
|
+
contentHash: postScanHash,
|
|
76
|
+
risk: Math.max(warning.risk, rescanned.risk),
|
|
77
|
+
reasons: [...warning.reasons, 'File changed during re-scan (possible TOCTOU)'],
|
|
78
|
+
detectedAt: new Date(now()).toISOString(),
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (rescanned.decision === 'suspicious') {
|
|
83
|
+
const rescannedWarning = {
|
|
84
|
+
skillName: rescanned.skillName,
|
|
85
|
+
skillPath: rescanned.skillPath,
|
|
86
|
+
contentHash: rescanned.contentHash,
|
|
87
|
+
risk: rescanned.risk,
|
|
88
|
+
reasons: [...rescanned.reasons],
|
|
89
|
+
detectedAt: new Date(now()).toISOString(),
|
|
90
|
+
};
|
|
91
|
+
if (await warningStore.isDismissed(rescannedWarning.skillName, rescannedWarning.contentHash)) {
|
|
92
|
+
summary.removed += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
deliverWarnings.push(rescannedWarning);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
summary.removed += 1;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const nodeError = error;
|
|
102
|
+
if (nodeError?.code === 'ENOENT') {
|
|
103
|
+
summary.removed += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
summary.errors += 1;
|
|
107
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
108
|
+
writeError(stderr, `guard check failed for ${warning.skillPath}: ${reason}`);
|
|
109
|
+
nextPending.push(warning);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await warningStore.replacePending([...nextPending, ...deliverWarnings]);
|
|
113
|
+
summary.acknowledged = await warningStore.acknowledgeAll(deliverWarnings);
|
|
114
|
+
summary.delivered = deliverWarnings.length;
|
|
115
|
+
const payload = {
|
|
116
|
+
suppressPrompt: false,
|
|
117
|
+
message: deliverWarnings.length > 0 ? toWarningMessage(deliverWarnings) : '',
|
|
118
|
+
warnings: deliverWarnings,
|
|
119
|
+
summary: { ...summary },
|
|
120
|
+
};
|
|
121
|
+
writeJson(stdout, payload);
|
|
122
|
+
return payload;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
summary.errors += 1;
|
|
126
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
127
|
+
writeError(stderr, `guard check failed: ${reason}`);
|
|
128
|
+
const payload = {
|
|
129
|
+
suppressPrompt: false,
|
|
130
|
+
message: '',
|
|
131
|
+
warnings: [],
|
|
132
|
+
summary: { ...summary },
|
|
133
|
+
};
|
|
134
|
+
writeJson(stdout, payload);
|
|
135
|
+
return payload;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Dirent } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { ScanCache } from '../ScanCache';
|
|
4
|
+
import { WarningStore } from '../WarningStore';
|
|
5
|
+
import type { GuardHookOutput, OutputWriter, SingleSkillScanResult } from '../types';
|
|
6
|
+
type ReaddirWithFileTypes = (filePath: string, options: {
|
|
7
|
+
withFileTypes: true;
|
|
8
|
+
encoding: BufferEncoding;
|
|
9
|
+
}) => Promise<Dirent[]>;
|
|
10
|
+
export interface GuardScanOptions {
|
|
11
|
+
watchPaths?: string[];
|
|
12
|
+
currentWorkingDirectory?: string;
|
|
13
|
+
homeDir?: string;
|
|
14
|
+
stdout?: OutputWriter;
|
|
15
|
+
stderr?: OutputWriter;
|
|
16
|
+
scanCache?: ScanCache;
|
|
17
|
+
warningStore?: WarningStore;
|
|
18
|
+
scanSkillFn?: (filePath: string) => Promise<SingleSkillScanResult>;
|
|
19
|
+
readFileFn?: (filePath: string, encoding: BufferEncoding) => Promise<string>;
|
|
20
|
+
realpathFn?: (filePath: string) => Promise<string>;
|
|
21
|
+
readdirFn?: ReaddirWithFileTypes;
|
|
22
|
+
statFn?: typeof stat;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export declare function guardScan(options?: GuardScanOptions): Promise<GuardHookOutput>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=guardScan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guardScan.d.ts","sourceRoot":"","sources":["../../../src/guard/hooks/guardScan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAA+B,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAMpE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAYpF,KAAK,oBAAoB,GAAG,CAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IACP,aAAa,EAAE,IAAI,CAAA;IACnB,QAAQ,EAAE,cAAc,CAAA;CACzB,KACE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;AAEtB,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAClE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClD,SAAS,CAAC,EAAE,oBAAoB,CAAA;IAChC,MAAM,CAAC,EAAE,OAAO,IAAI,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAyJD,wBAAsB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAmHxF"}
|