dsh-code-security 0.3.8 → 0.4.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/CHANGELOG.md +9 -0
- package/README.en.md +5 -4
- package/README.md +5 -4
- package/THIRD_PARTY_NOTICES.md +23 -0
- package/docs/USAGE.en.md +80 -23
- package/docs/USAGE.md +68 -56
- package/docs/VALIDATION.md +14 -0
- package/lib/audit.d.ts +27 -0
- package/lib/audit.js +109 -0
- package/lib/automatic.d.ts +18 -0
- package/lib/automatic.js +103 -0
- package/lib/client.js +63 -0
- package/lib/config.d.ts +9 -0
- package/lib/config.js +12 -1
- package/lib/deps.d.ts +14 -8
- package/lib/deps.js +170 -143
- package/lib/engines.d.ts +50 -0
- package/lib/engines.js +255 -0
- package/lib/extra.d.ts +2 -1
- package/lib/extra.js +29 -4
- package/lib/index.d.ts +8 -2
- package/lib/index.js +45 -5
- package/lib/privacy.d.ts +6 -0
- package/lib/privacy.js +39 -0
- package/lib/rules.js +1 -1
- package/lib/runner.d.ts +4 -0
- package/lib/runner.js +8 -3
- package/lib/scanner.d.ts +12 -1
- package/lib/scanner.js +22 -5
- package/lib/state.d.ts +7 -0
- package/lib/state.js +28 -7
- package/lib/syntax.d.ts +1 -0
- package/lib/syntax.js +43 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +53 -20
- package/lib/workbench.d.ts +30 -0
- package/lib/workbench.js +242 -0
- package/package.json +16 -3
- package/rules/betterleaks.toml +3 -0
- package/rules/osv.toml +1 -0
- package/rules/semgrep.yml +72 -0
package/lib/deps.js
CHANGED
|
@@ -1,179 +1,206 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SBOM-lite:解析常见依赖清单,输出依赖名称、版本约束与风险标记。
|
|
3
|
-
* 只做确定性解析,不联网、不执行包管理器。
|
|
4
|
-
*
|
|
5
|
-
* @module dsh-code-security/deps
|
|
6
|
-
*/
|
|
1
|
+
/** Offline manifest inventory. Parsing/coverage failures are explicit, never a clean audit. */
|
|
7
2
|
import fs from 'node:fs/promises';
|
|
8
3
|
import path from 'node:path';
|
|
4
|
+
import { parse as parseToml } from 'smol-toml';
|
|
9
5
|
import { DEFAULT_EXCLUDE_DIRS } from './config.js';
|
|
6
|
+
import { redactText } from './privacy.js';
|
|
7
|
+
import { policyExcludesDir } from './policy.js';
|
|
8
|
+
const LOCKFILES = ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'uv.lock', 'poetry.lock', 'Pipfile.lock', 'pdm.lock', 'pylock.toml', 'Cargo.lock', 'composer.lock', 'Gemfile.lock', 'go.mod', 'requirements.txt', 'pom.xml'];
|
|
10
9
|
const MANIFESTS = ['package.json', 'requirements.txt', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'composer.json', 'Gemfile'];
|
|
11
|
-
|
|
12
|
-
return typeof value === 'object' && value !== null ? value : {};
|
|
13
|
-
}
|
|
10
|
+
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
14
11
|
function riskOfSpec(spec) {
|
|
15
12
|
const s = spec.trim();
|
|
16
|
-
if (s
|
|
13
|
+
if (!s)
|
|
17
14
|
return { risk: 'unpinned', note: '未声明版本约束' };
|
|
18
|
-
if (
|
|
19
|
-
return { risk: '
|
|
20
|
-
if (/^(?:https?|git|
|
|
21
|
-
return
|
|
22
|
-
if (/^(
|
|
23
|
-
return { risk: '
|
|
24
|
-
if (/^
|
|
15
|
+
if (/^(?:file|link|workspace|path):/i.test(s))
|
|
16
|
+
return { risk: 'local', note: '本地或工作区依赖' };
|
|
17
|
+
if (/^(?:https?|git|github:)/i.test(s))
|
|
18
|
+
return /#[a-f0-9]{7,40}$/i.test(s) ? { risk: 'exact', note: 'Git 提交已固定' } : { risk: 'git', note: '外部源未固定 Git 提交' };
|
|
19
|
+
if (s === '*' || /^latest$/i.test(s) || /(?:^|\.)[x*](?:\.|$)/i.test(s))
|
|
20
|
+
return { risk: 'wildcard', note: '浮动版本' };
|
|
21
|
+
if (/^(?:==)?v?\d+(?:\.\d+)+(?:[-+][\w.-]+)?$/.test(s))
|
|
25
22
|
return { risk: 'exact', note: '' };
|
|
26
23
|
if (/[<>=~^]/.test(s) || s.includes('||'))
|
|
27
24
|
return { risk: 'range', note: '' };
|
|
28
|
-
return { risk: 'unpinned', note: '
|
|
29
|
-
}
|
|
30
|
-
function pushDeps(list, entries, manager, file, kind) {
|
|
31
|
-
for (const [name, rawSpec] of Object.entries(entries)) {
|
|
32
|
-
const spec = typeof rawSpec === 'string' ? rawSpec : '';
|
|
33
|
-
const { risk, note } = riskOfSpec(spec);
|
|
34
|
-
list.push({ name, spec, manager, file, kind, risk, note });
|
|
35
|
-
}
|
|
25
|
+
return { risk: 'unpinned', note: '未能识别固定版本' };
|
|
36
26
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return list;
|
|
27
|
+
export async function scanDeps(root, options = {}) {
|
|
28
|
+
const dependencies = [], warnings = [];
|
|
29
|
+
const lockfiles = [];
|
|
30
|
+
let filesScanned = 0, filesSkipped = 0, visited = 0, complete = true;
|
|
31
|
+
const warn = (text) => { complete = false; if (warnings.length < 100)
|
|
32
|
+
warnings.push(text); };
|
|
33
|
+
function add(name, rawSpec, manager, file, kind) {
|
|
34
|
+
dependencies.push({ name: redactText(name), spec: redactText(rawSpec), manager, file, kind, ...riskOfSpec(rawSpec) });
|
|
46
35
|
}
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
function table(value, manager, file, kind) {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return;
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
40
|
+
warn(file + ' 的依赖表格式无效');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const [name, valueSpec] of Object.entries(record(value))) {
|
|
44
|
+
if (typeof valueSpec !== 'string' && (!valueSpec || typeof valueSpec !== 'object' || Array.isArray(valueSpec))) {
|
|
45
|
+
warn(file + ' 含无效依赖声明');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const obj = record(valueSpec);
|
|
49
|
+
const spec = typeof valueSpec === 'string' ? valueSpec : typeof obj.git === 'string' ? obj.git + (typeof obj.rev === 'string' ? '#' + obj.rev : '') : typeof obj.path === 'string' ? 'path:' + obj.path : typeof obj.version === 'string' ? obj.version : '';
|
|
50
|
+
add(name, spec, manager, file, kind);
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const { risk, note } = riskOfSpec(spec);
|
|
62
|
-
list.push({ name: m[1], spec, manager: 'pypi', file, kind: 'runtime', risk, note });
|
|
53
|
+
function requirement(value, file, kind) {
|
|
54
|
+
if (typeof value !== 'string') {
|
|
55
|
+
warn(file + ' 含尚未展开的依赖组引用');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const match = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]+\])?\s*(.*)$/.exec(value);
|
|
59
|
+
if (!match) {
|
|
60
|
+
warn(file + ' 有未识别的 Python 依赖');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
add(match[1], match[2].split(';', 1)[0].trim().replace(/^@\s*/, ''), 'pypi', file, kind);
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for (const
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
function pyproject(text, file) {
|
|
66
|
+
const doc = parseToml(text), project = record(doc.project);
|
|
67
|
+
if (project.dependencies !== undefined && !Array.isArray(project.dependencies))
|
|
68
|
+
throw new Error('invalid dependencies');
|
|
69
|
+
for (const item of project.dependencies ?? [])
|
|
70
|
+
requirement(item, file, 'runtime');
|
|
71
|
+
for (const items of Object.values(record(project['optional-dependencies']))) {
|
|
72
|
+
if (!Array.isArray(items)) {
|
|
73
|
+
warn(file + ' 可选依赖不是数组');
|
|
73
74
|
continue;
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
}
|
|
76
|
+
items.forEach(item => requirement(item, file, 'optional'));
|
|
77
|
+
}
|
|
78
|
+
for (const items of Object.values(record(doc['dependency-groups']))) {
|
|
79
|
+
if (!Array.isArray(items)) {
|
|
80
|
+
warn(file + ' 依赖组不是数组');
|
|
76
81
|
continue;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
list.push({ name: m[1], spec, manager: 'pypi', file, kind: 'runtime', risk, note });
|
|
82
|
+
}
|
|
83
|
+
items.forEach(item => requirement(item, file, 'dev'));
|
|
80
84
|
}
|
|
85
|
+
const poetry = record(record(doc.tool).poetry);
|
|
86
|
+
const poetryTable = (value, kind) => table(Object.fromEntries(Object.entries(record(value)).filter(([name]) => name !== 'python')), 'pypi', file, kind);
|
|
87
|
+
poetryTable(poetry.dependencies, 'runtime');
|
|
88
|
+
poetryTable(poetry['dev-dependencies'], 'dev');
|
|
89
|
+
for (const group of Object.values(record(poetry.group)))
|
|
90
|
+
poetryTable(record(group).dependencies, 'dev');
|
|
91
|
+
if (Array.isArray(project.dynamic) && project.dynamic.includes('dependencies'))
|
|
92
|
+
warn(file + ' 的 dependencies 为动态声明,未执行项目代码读取');
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const line of text.split(/\r?\n/)) {
|
|
95
|
-
const m = /^\s*([A-Za-z0-9_.\-]+)\s*=\s*(.+)$/.exec(line);
|
|
96
|
-
if (m === null)
|
|
97
|
-
continue;
|
|
98
|
-
const spec = m[2].trim().replace(/^["']|["']$/g, '');
|
|
99
|
-
const isGit = /git\s*=|branch\s*=|rev\s*=/.test(spec);
|
|
100
|
-
const risk = isGit ? (spec.includes('rev') ? 'exact' : 'git') : 'range';
|
|
101
|
-
list.push({ name: m[1], spec, manager: 'cargo', file, kind: 'runtime', risk, note: isGit && !spec.includes('rev') ? 'git 源未锁定 rev' : '' });
|
|
102
|
-
}
|
|
103
|
-
return list;
|
|
104
|
-
}
|
|
105
|
-
function parseComposer(text, file) {
|
|
106
|
-
const list = [];
|
|
107
|
-
try {
|
|
108
|
-
const pkg = JSON.parse(text);
|
|
109
|
-
for (const [key, entries] of [['require', 'runtime'], ['require-dev', 'dev']]) {
|
|
110
|
-
pushDeps(list, asRecord(pkg[key]), 'composer', file, entries);
|
|
94
|
+
function parse(text, base, file) {
|
|
95
|
+
if (base === 'package.json' || base === 'composer.json') {
|
|
96
|
+
const pkg = JSON.parse(text);
|
|
97
|
+
if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg))
|
|
98
|
+
throw new Error('invalid manifest');
|
|
99
|
+
if (base === 'package.json')
|
|
100
|
+
for (const [field, kind] of [['dependencies', 'runtime'], ['devDependencies', 'dev'], ['peerDependencies', 'peer'], ['optionalDependencies', 'optional']])
|
|
101
|
+
table(pkg[field], 'npm', file, kind);
|
|
102
|
+
else {
|
|
103
|
+
table(pkg.require, 'composer', file, 'runtime');
|
|
104
|
+
table(pkg['require-dev'], 'composer', file, 'dev');
|
|
105
|
+
}
|
|
111
106
|
}
|
|
107
|
+
else if (base === 'pyproject.toml')
|
|
108
|
+
pyproject(text, file);
|
|
109
|
+
else if (base === 'requirements.txt')
|
|
110
|
+
for (const line of text.split(/\r?\n/)) {
|
|
111
|
+
const item = line.trim();
|
|
112
|
+
if (!item || item.startsWith('#'))
|
|
113
|
+
continue;
|
|
114
|
+
if (item.startsWith('-')) {
|
|
115
|
+
warn(file + ' 包含外部文件/安装选项,未执行解析');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
requirement(item.split(/\s+#/, 1)[0], file, 'runtime');
|
|
119
|
+
}
|
|
120
|
+
else if (base === 'Cargo.toml') {
|
|
121
|
+
const doc = parseToml(text);
|
|
122
|
+
const cargo = (value) => { const obj = record(value); for (const [field, kind] of [['dependencies', 'runtime'], ['dev-dependencies', 'dev'], ['build-dependencies', 'dev']])
|
|
123
|
+
table(obj[field], 'cargo', file, kind); };
|
|
124
|
+
cargo(doc);
|
|
125
|
+
cargo(record(doc.workspace));
|
|
126
|
+
Object.values(record(doc.target)).forEach(cargo);
|
|
127
|
+
}
|
|
128
|
+
else if (base === 'go.mod') {
|
|
129
|
+
for (const m of text.matchAll(/^\s*(?:require\s+)?([A-Za-z0-9_.\/-]+)\s+(v[0-9][^\s]*)/gm))
|
|
130
|
+
add(m[1], m[2], 'go', file, 'runtime');
|
|
131
|
+
if (/^\s*replace\s/m.test(text))
|
|
132
|
+
warn(file + ' 含 replace,清单不代表最终解析图');
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
for (const m of text.matchAll(/^\s*gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]*)['"]\s*)?/gm))
|
|
136
|
+
add(m[1], m[2] ?? '', 'ruby', file, 'runtime');
|
|
112
137
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
list.push({ name: m[1], spec, manager: 'ruby', file, kind: 'runtime', risk, note });
|
|
124
|
-
}
|
|
125
|
-
return list;
|
|
126
|
-
}
|
|
127
|
-
/** 扫描依赖清单,返回 SBOM-lite 结果。 */
|
|
128
|
-
export async function scanDeps(root) {
|
|
129
|
-
const dependencies = [];
|
|
130
|
-
const warnings = [];
|
|
131
|
-
let filesScanned = 0;
|
|
132
|
-
const visit = async (absolute, rel) => {
|
|
138
|
+
async function visit(absolute, rel) {
|
|
139
|
+
options.signal?.throwIfAborted();
|
|
140
|
+
if (++visited > 100000 || filesScanned >= (options.maxFiles ?? 5000)) {
|
|
141
|
+
warn('已达到扫描数量上限');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (rel.split('/').some(p => DEFAULT_EXCLUDE_DIRS.includes(p)) || options.policy && policyExcludesDir(options.policy, rel)) {
|
|
145
|
+
filesSkipped++;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
133
148
|
let stat;
|
|
134
149
|
try {
|
|
135
|
-
stat = await fs.
|
|
150
|
+
stat = await fs.lstat(absolute);
|
|
136
151
|
}
|
|
137
152
|
catch {
|
|
153
|
+
filesSkipped++;
|
|
154
|
+
warn('无法读取 ' + rel);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (stat.isSymbolicLink()) {
|
|
158
|
+
filesSkipped++;
|
|
159
|
+
if (absolute === path.resolve(root))
|
|
160
|
+
warn('目标为链接,未读取链接内容');
|
|
138
161
|
return;
|
|
139
162
|
}
|
|
140
163
|
if (stat.isDirectory()) {
|
|
141
|
-
|
|
164
|
+
let entries;
|
|
165
|
+
try {
|
|
166
|
+
entries = await fs.readdir(absolute, { withFileTypes: true });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
warn('无法遍历 ' + rel);
|
|
142
170
|
return;
|
|
143
|
-
|
|
144
|
-
for (const entry of entries)
|
|
145
|
-
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (visited > 100000 || filesScanned >= (options.maxFiles ?? 5000)) {
|
|
174
|
+
warn('已达到扫描数量上限');
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
;
|
|
178
|
+
await visit(path.join(absolute, entry.name), rel ? rel + '/' + entry.name : entry.name);
|
|
179
|
+
}
|
|
146
180
|
return;
|
|
147
181
|
}
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
if (!stat.isFile() || !MANIFESTS.includes(path.basename(absolute)) && !LOCKFILES.includes(path.basename(absolute)))
|
|
183
|
+
return;
|
|
184
|
+
if (stat.size > (options.maxFileBytes ?? 2 * 1024 * 1024)) {
|
|
185
|
+
filesSkipped++;
|
|
186
|
+
warn(rel + ' 超过文件大小上限');
|
|
150
187
|
return;
|
|
151
|
-
|
|
152
|
-
|
|
188
|
+
}
|
|
189
|
+
filesScanned++;
|
|
153
190
|
try {
|
|
154
|
-
text = await fs.readFile(absolute, 'utf8');
|
|
191
|
+
const text = await fs.readFile(absolute, { encoding: 'utf8', signal: options.signal }), file = rel || path.basename(absolute);
|
|
192
|
+
if (LOCKFILES.includes(path.basename(absolute))) {
|
|
193
|
+
lockfiles.push(file);
|
|
194
|
+
options.onDocument?.({ file, text });
|
|
195
|
+
}
|
|
196
|
+
if (MANIFESTS.includes(path.basename(absolute)))
|
|
197
|
+
parse(text, path.basename(absolute), file);
|
|
155
198
|
}
|
|
156
199
|
catch {
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
options.signal?.throwIfAborted();
|
|
201
|
+
warn('无法解析 ' + (rel || path.basename(absolute)) + ':清单格式无效,本文件不计作已确认安全');
|
|
159
202
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
dependencies.push(...parseRequirements(text, rel));
|
|
164
|
-
else if (base === 'pyproject.toml')
|
|
165
|
-
dependencies.push(...parsePyproject(text, rel));
|
|
166
|
-
else if (base === 'go.mod')
|
|
167
|
-
dependencies.push(...parseGoMod(text, rel));
|
|
168
|
-
else if (base === 'Cargo.toml')
|
|
169
|
-
dependencies.push(...parseCargo(text, rel));
|
|
170
|
-
else if (base === 'composer.json')
|
|
171
|
-
dependencies.push(...parseComposer(text, rel));
|
|
172
|
-
else if (base === 'Gemfile')
|
|
173
|
-
dependencies.push(...parseGemfile(text, rel));
|
|
174
|
-
};
|
|
175
|
-
await visit(root, '');
|
|
176
|
-
dependencies.sort((a, b) => a.file.localeCompare(b.file) || a.name.localeCompare(b.name));
|
|
177
|
-
const risky = dependencies.filter((item) => item.risk !== 'exact' && item.risk !== 'range');
|
|
178
|
-
return { root, filesScanned, dependencies, riskyCount: risky.length, warnings };
|
|
203
|
+
}
|
|
204
|
+
await visit(path.resolve(root), '');
|
|
205
|
+
return { root, filesScanned, filesSkipped, dependencies, riskyCount: dependencies.filter(item => !['exact', 'range'].includes(item.risk)).length, warnings, complete, lockfiles };
|
|
179
206
|
}
|
package/lib/engines.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ResolvedSecureConfig, SourceEngine } from './config.js';
|
|
2
|
+
import type { ProcessRunner } from './runner.js';
|
|
3
|
+
import type { Finding } from './scanner.js';
|
|
4
|
+
export type EngineId = SourceEngine | 'osv';
|
|
5
|
+
export interface EngineCoverage {
|
|
6
|
+
engine: EngineId;
|
|
7
|
+
status: 'completed' | 'partial' | 'not-installed' | 'failed' | 'timed-out' | 'not-run';
|
|
8
|
+
version?: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
findingsCount?: number;
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ScanDocument {
|
|
14
|
+
file: string;
|
|
15
|
+
text: string;
|
|
16
|
+
selectedLines?: ReadonlySet<number>;
|
|
17
|
+
}
|
|
18
|
+
/** File modification dates describe the cache, not the age of every advisory inside it. */
|
|
19
|
+
export declare function databaseHealth(cfg: ResolvedSecureConfig): Promise<{
|
|
20
|
+
directory: string;
|
|
21
|
+
available: boolean;
|
|
22
|
+
databases: {
|
|
23
|
+
ecosystem: string;
|
|
24
|
+
fileModifiedAt: string;
|
|
25
|
+
}[];
|
|
26
|
+
}>;
|
|
27
|
+
export declare function probeEngine(engine: Exclude<EngineId, 'builtin'>, cfg: ResolvedSecureConfig, runner: ProcessRunner, signal?: AbortSignal): Promise<EngineCoverage>;
|
|
28
|
+
export declare function scanExternal(engine: 'betterleaks' | 'semgrep', documents: ScanDocument[], cfg: ResolvedSecureConfig, runner: ProcessRunner, signal?: AbortSignal): Promise<{
|
|
29
|
+
findings: Finding[];
|
|
30
|
+
coverage: EngineCoverage;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function mergeFindings(findings: Finding[]): Finding[];
|
|
33
|
+
export declare function auditDependencies(documents: ScanDocument[], mode: 'offline' | 'online', cfg: ResolvedSecureConfig, runner: ProcessRunner, signal?: AbortSignal): Promise<{
|
|
34
|
+
mode: "offline" | "online";
|
|
35
|
+
coverage: EngineCoverage;
|
|
36
|
+
packagesScanned: number;
|
|
37
|
+
vulnerabilities: never[];
|
|
38
|
+
} | {
|
|
39
|
+
mode: "offline" | "online";
|
|
40
|
+
coverage: {
|
|
41
|
+
findingsCount: number;
|
|
42
|
+
reason?: string;
|
|
43
|
+
status: "completed" | "partial" | "not-run";
|
|
44
|
+
engine: EngineId;
|
|
45
|
+
version?: string;
|
|
46
|
+
durationMs?: number;
|
|
47
|
+
};
|
|
48
|
+
packagesScanned: number;
|
|
49
|
+
vulnerabilities: Record<string, unknown>[];
|
|
50
|
+
}>;
|