@vltpkg/query 1.0.0-rc.33 → 1.0.0-rc.34
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/index.js +10 -6
- package/dist/parser.js +2 -1
- package/dist/pseudo/malware.d.ts +3 -16
- package/dist/pseudo/malware.js +11 -171
- package/dist/pseudo/vuln.d.ts +22 -0
- package/dist/pseudo/vuln.js +220 -0
- package/dist/pseudo.js +3 -3
- package/dist/types.d.ts +3 -2
- package/package.json +8 -8
- package/skills/dss-query/REFERENCE.md +8 -4
- package/skills/dss-query/SKILL.md +7 -7
- package/src/index.ts +21 -14
- package/src/parser.ts +2 -1
- package/src/pseudo/malware.ts +13 -221
- package/src/pseudo/vuln.ts +273 -0
- package/src/pseudo.ts +3 -3
- package/src/types.ts +3 -2
- package/tap-snapshots/test/pseudo/malware.ts.test.cjs +1 -170
- package/tap-snapshots/test/pseudo/vuln.ts.test.cjs +208 -0
- package/tap-snapshots/test/pseudo/vulnerable.ts.test.cjs +3 -1
- package/test/index.ts +8 -0
- package/test/pseudo/malware.ts +65 -312
- package/test/pseudo/vuln.ts +625 -0
- package/test/pseudo/vulnerable.ts +42 -35
- package/tsconfig.publish.tsbuildinfo +1 -1
- package/dist/pseudo/vulnerable.d.ts +0 -8
- package/dist/pseudo/vulnerable.js +0 -17
- package/src/pseudo/vulnerable.ts +0 -26
package/dist/index.js
CHANGED
|
@@ -124,6 +124,8 @@ const securitySelectors = new Set([
|
|
|
124
124
|
':unmaintained',
|
|
125
125
|
':unpopular',
|
|
126
126
|
':unstable',
|
|
127
|
+
':vuln',
|
|
128
|
+
':vulnerable',
|
|
127
129
|
]);
|
|
128
130
|
const setMethodToJSON = (node) => {
|
|
129
131
|
const { toJSON } = node;
|
|
@@ -234,12 +236,7 @@ export class Query {
|
|
|
234
236
|
none: securityArchiveEntry.alerts.some(i => i.type === 'noLicenseFound'),
|
|
235
237
|
exception: securityArchiveEntry.alerts.some(i => i.type === 'licenseException'),
|
|
236
238
|
},
|
|
237
|
-
malware:
|
|
238
|
-
low: securityArchiveEntry.alerts.some(i => i.type === 'gptAnomaly'),
|
|
239
|
-
medium: securityArchiveEntry.alerts.some(i => i.type === 'gptSecurity'),
|
|
240
|
-
high: securityArchiveEntry.alerts.some(i => i.type === 'gptMalware'),
|
|
241
|
-
critical: securityArchiveEntry.alerts.some(i => i.type === 'malware'),
|
|
242
|
-
},
|
|
239
|
+
malware: securityArchiveEntry.alerts.some(i => i.type === 'malware' || i.type === 'gptMalware'),
|
|
243
240
|
minified: securityArchiveEntry.alerts.some(i => i.type === 'minifiedFile'),
|
|
244
241
|
native: securityArchiveEntry.alerts.some(i => i.type === 'hasNativeCode'),
|
|
245
242
|
network: securityArchiveEntry.alerts.some(i => i.type === 'networkAccess'),
|
|
@@ -265,6 +262,13 @@ export class Query {
|
|
|
265
262
|
unmaintained: securityArchiveEntry.alerts.some(i => i.type === 'unmaintained'),
|
|
266
263
|
unpopular: securityArchiveEntry.alerts.some(i => i.type === 'unpopularPackage'),
|
|
267
264
|
unstable: securityArchiveEntry.alerts.some(i => i.type === 'unstableOwnership'),
|
|
265
|
+
vuln: {
|
|
266
|
+
low: securityArchiveEntry.alerts.some(i => i.type === 'mildCVE' || i.type === 'gptAnomaly'),
|
|
267
|
+
medium: securityArchiveEntry.alerts.some(i => i.type === 'potentialVulnerability' ||
|
|
268
|
+
i.type === 'gptSecurity'),
|
|
269
|
+
high: securityArchiveEntry.alerts.some(i => i.type === 'cve'),
|
|
270
|
+
critical: securityArchiveEntry.alerts.some(i => i.type === 'criticalCVE'),
|
|
271
|
+
},
|
|
268
272
|
};
|
|
269
273
|
setMethodToJSON(node);
|
|
270
274
|
}
|
package/dist/parser.js
CHANGED
|
@@ -11,12 +11,13 @@ export const unescapeDots = (query) => query.replaceAll('\\.', '.');
|
|
|
11
11
|
const pseudoCleanUpNeeded = new Set([
|
|
12
12
|
':published',
|
|
13
13
|
':score',
|
|
14
|
-
':malware',
|
|
15
14
|
':severity',
|
|
16
15
|
':sev',
|
|
17
16
|
':squat',
|
|
18
17
|
':semver',
|
|
19
18
|
':v',
|
|
19
|
+
':vuln',
|
|
20
|
+
':vulnerable',
|
|
20
21
|
':path',
|
|
21
22
|
]);
|
|
22
23
|
const hasParamsToEscape = (node) => pseudoCleanUpNeeded.has(node.value);
|
package/dist/pseudo/malware.d.ts
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import type { ParserState } from '../types.ts';
|
|
2
|
-
import type { PostcssNode } from '@vltpkg/dss-parser';
|
|
3
|
-
export type MalwareKinds = '0' | '1' | '2' | '3' | 'critical' | 'high' | 'medium' | 'low' | undefined;
|
|
4
|
-
export type MalwareAlertTypes = 'malware' | 'gptMalware' | 'gptSecurity' | 'gptAnomaly' | undefined;
|
|
5
|
-
export type MalwareComparator = '>' | '<' | '>=' | '<=' | undefined;
|
|
6
|
-
export declare const isMalwareKind: (value?: string) => value is MalwareKinds;
|
|
7
|
-
export declare const asMalwareKind: (value?: string) => MalwareKinds;
|
|
8
|
-
export declare const parseInternals: (nodes: PostcssNode[]) => {
|
|
9
|
-
kind: MalwareKinds;
|
|
10
|
-
comparator: MalwareComparator;
|
|
11
|
-
};
|
|
12
2
|
/**
|
|
13
|
-
* :malware Pseudo-Selector, matches nodes with malware
|
|
3
|
+
* :malware Pseudo-Selector, matches nodes with any malware-related alert.
|
|
14
4
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - :malware(critical) - matches specific malware kind
|
|
18
|
-
* - :malware(>1) - matches malware with severity greater than 1
|
|
19
|
-
* - :malware(">=medium") - matches malware with severity >= medium
|
|
5
|
+
* This is a binary selector — no parameters. A node matches if it has
|
|
6
|
+
* at least one alert whose type is `malware` or `gptMalware`.
|
|
20
7
|
*/
|
|
21
8
|
export declare const malware: (state: ParserState) => Promise<ParserState & {
|
|
22
9
|
securityArchive: NonNullable<ParserState["securityArchive"]>;
|
package/dist/pseudo/malware.js
CHANGED
|
@@ -1,183 +1,23 @@
|
|
|
1
1
|
import { error } from '@vltpkg/error-cause';
|
|
2
|
-
import { asPostcssNodeWithChildren
|
|
3
|
-
import { assertSecurityArchive, removeDanglingEdges, removeNode,
|
|
4
|
-
const
|
|
5
|
-
['critical', 'malware'],
|
|
6
|
-
['high', 'gptMalware'],
|
|
7
|
-
['medium', 'gptSecurity'],
|
|
8
|
-
['low', 'gptAnomaly'],
|
|
9
|
-
['0', 'malware'],
|
|
10
|
-
['1', 'gptMalware'],
|
|
11
|
-
['2', 'gptSecurity'],
|
|
12
|
-
['3', 'gptAnomaly'],
|
|
13
|
-
]);
|
|
14
|
-
// Map numerical values to their respective kinds for comparison operations
|
|
15
|
-
const kindLevelMap = new Map([
|
|
16
|
-
['critical', 0],
|
|
17
|
-
['high', 1],
|
|
18
|
-
['medium', 2],
|
|
19
|
-
['low', 3],
|
|
20
|
-
['0', 0],
|
|
21
|
-
['1', 1],
|
|
22
|
-
['2', 2],
|
|
23
|
-
['3', 3],
|
|
24
|
-
]);
|
|
25
|
-
const kinds = new Set(kindsMap.keys());
|
|
26
|
-
export const isMalwareKind = (value) => kinds.has(value);
|
|
27
|
-
export const asMalwareKind = (value) => {
|
|
28
|
-
if (!isMalwareKind(value)) {
|
|
29
|
-
throw error('Expected a valid malware kind', {
|
|
30
|
-
found: value,
|
|
31
|
-
validOptions: Array.from(kinds),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
};
|
|
36
|
-
export const parseInternals = (nodes) => {
|
|
37
|
-
// Handle case where no parameters are provided (parameterless :malware)
|
|
38
|
-
if (!nodes[0]) {
|
|
39
|
-
return { kind: undefined, comparator: undefined };
|
|
40
|
-
}
|
|
41
|
-
const selectorNode = asPostcssNodeWithChildren(nodes[0]);
|
|
42
|
-
if (!selectorNode.nodes[0]) {
|
|
43
|
-
return { kind: undefined, comparator: undefined };
|
|
44
|
-
}
|
|
45
|
-
let kindValue = '';
|
|
46
|
-
let comparator = undefined;
|
|
47
|
-
let kind;
|
|
48
|
-
// Parse the parameter (kind with optional comparator)
|
|
49
|
-
if (isStringNode(selectorNode.nodes[0])) {
|
|
50
|
-
kindValue = removeQuotes(asStringNode(selectorNode.nodes[0]).value);
|
|
51
|
-
}
|
|
52
|
-
else if (isTagNode(selectorNode.nodes[0])) {
|
|
53
|
-
kindValue = asTagNode(selectorNode.nodes[0]).value;
|
|
54
|
-
}
|
|
55
|
-
// Extract comparator if present
|
|
56
|
-
if (kindValue.startsWith('>=')) {
|
|
57
|
-
comparator = '>=';
|
|
58
|
-
kindValue = kindValue.substring(2);
|
|
59
|
-
}
|
|
60
|
-
else if (kindValue.startsWith('<=')) {
|
|
61
|
-
comparator = '<=';
|
|
62
|
-
kindValue = kindValue.substring(2);
|
|
63
|
-
}
|
|
64
|
-
else if (kindValue.startsWith('>')) {
|
|
65
|
-
comparator = '>';
|
|
66
|
-
kindValue = kindValue.substring(1);
|
|
67
|
-
}
|
|
68
|
-
else if (kindValue.startsWith('<')) {
|
|
69
|
-
comparator = '<';
|
|
70
|
-
kindValue = kindValue.substring(1);
|
|
71
|
-
}
|
|
72
|
-
// Validate the kind without comparator
|
|
73
|
-
if (!comparator) {
|
|
74
|
-
kind = asMalwareKind(kindValue);
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
// For comparisons, just make sure it's a valid numeric value or a valid kind
|
|
78
|
-
if (isMalwareKind(kindValue)) {
|
|
79
|
-
kind = kindValue;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
throw error('Expected a valid malware kind or number between 0-3', {
|
|
83
|
-
found: kindValue,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return { kind, comparator };
|
|
88
|
-
};
|
|
2
|
+
import { asPostcssNodeWithChildren } from '@vltpkg/dss-parser';
|
|
3
|
+
import { assertSecurityArchive, removeDanglingEdges, removeNode, } from "./helpers.js";
|
|
4
|
+
const malwareAlertTypes = new Set(['malware', 'gptMalware']);
|
|
89
5
|
/**
|
|
90
|
-
* :malware Pseudo-Selector, matches nodes with malware
|
|
6
|
+
* :malware Pseudo-Selector, matches nodes with any malware-related alert.
|
|
91
7
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* - :malware(critical) - matches specific malware kind
|
|
95
|
-
* - :malware(>1) - matches malware with severity greater than 1
|
|
96
|
-
* - :malware(">=medium") - matches malware with severity >= medium
|
|
8
|
+
* This is a binary selector — no parameters. A node matches if it has
|
|
9
|
+
* at least one alert whose type is `malware` or `gptMalware`.
|
|
97
10
|
*/
|
|
98
11
|
export const malware = async (state) => {
|
|
99
12
|
assertSecurityArchive(state, 'malware');
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
throw error('Failed to parse :malware selector', { cause: err });
|
|
13
|
+
const currentNode = asPostcssNodeWithChildren(state.current);
|
|
14
|
+
if (currentNode.nodes.length > 0) {
|
|
15
|
+
throw error(':malware selector does not accept parameters. Use :vuln(<severity>) for severity-based filtering.');
|
|
106
16
|
}
|
|
107
|
-
const { kind, comparator } = internals;
|
|
108
|
-
const alertName = comparator ? undefined : kindsMap.get(kind);
|
|
109
17
|
for (const node of state.partial.nodes) {
|
|
110
18
|
const report = state.securityArchive.get(node.id);
|
|
111
|
-
|
|
112
|
-
if (!
|
|
113
|
-
removeNode(state, node);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
for (const node of state.partial.nodes) {
|
|
117
|
-
const report = state.securityArchive.get(node.id);
|
|
118
|
-
let exclude = true;
|
|
119
|
-
if (report) {
|
|
120
|
-
if (kind === undefined && comparator === undefined) {
|
|
121
|
-
// Parameterless :malware - match malware alerts with severity >= medium (exclude low/gptAnomaly)
|
|
122
|
-
exclude = !report.alerts.some(alert => alert.type === 'malware' ||
|
|
123
|
-
alert.type === 'gptMalware' ||
|
|
124
|
-
alert.type === 'gptSecurity');
|
|
125
|
-
}
|
|
126
|
-
else if (comparator) {
|
|
127
|
-
// retrieve the value to compare against
|
|
128
|
-
const kindLevel = kindLevelMap.get(kind);
|
|
129
|
-
// the kindLevel value has already been validated at this point
|
|
130
|
-
// and thus can never return an undefined/falsy value but ts doesn't
|
|
131
|
-
// know about that, so we have the extra check here
|
|
132
|
-
/* c8 ignore next - impossible */
|
|
133
|
-
if (kindLevel == null)
|
|
134
|
-
break;
|
|
135
|
-
// Check each alert to find any that match our comparison criteria
|
|
136
|
-
for (const alert of report.alerts) {
|
|
137
|
-
// Get the numerical value of the alert type
|
|
138
|
-
const alertType = alert.type;
|
|
139
|
-
// retrieve a key to the current alert level to be compared against
|
|
140
|
-
const currentAlertLevelKey = [...kindsMap.entries()].find(([_, alertValue]) => alertValue === alertType)?.[0];
|
|
141
|
-
// perform the comparison based on the user-provided kindLevel
|
|
142
|
-
if (currentAlertLevelKey) {
|
|
143
|
-
const currentAlertLevel = kindLevelMap.get(currentAlertLevelKey);
|
|
144
|
-
/* c8 ignore next - impossible but ts doesn't know */
|
|
145
|
-
if (currentAlertLevel == null)
|
|
146
|
-
continue;
|
|
147
|
-
switch (comparator) {
|
|
148
|
-
case '>':
|
|
149
|
-
if (currentAlertLevel > kindLevel) {
|
|
150
|
-
exclude = false;
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
case '<':
|
|
154
|
-
if (currentAlertLevel < kindLevel) {
|
|
155
|
-
exclude = false;
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
case '>=':
|
|
159
|
-
if (currentAlertLevel >= kindLevel) {
|
|
160
|
-
exclude = false;
|
|
161
|
-
}
|
|
162
|
-
break;
|
|
163
|
-
case '<=':
|
|
164
|
-
if (currentAlertLevel <= kindLevel) {
|
|
165
|
-
exclude = false;
|
|
166
|
-
}
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
// If we've found a match, no need to check other alerts
|
|
170
|
-
if (!exclude)
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Original exact match behavior
|
|
177
|
-
exclude = !report.alerts.some(alert => alert.type === alertName);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (exclude) {
|
|
19
|
+
const hasMalware = report?.alerts.some(alert => malwareAlertTypes.has(alert.type));
|
|
20
|
+
if (!hasMalware) {
|
|
181
21
|
removeNode(state, node);
|
|
182
22
|
}
|
|
183
23
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ParserState } from '../types.ts';
|
|
2
|
+
import type { PostcssNode } from '@vltpkg/dss-parser';
|
|
3
|
+
export type VulnKinds = '0' | '1' | '2' | '3' | 'critical' | 'high' | 'medium' | 'low' | undefined;
|
|
4
|
+
export type VulnComparator = '>' | '<' | '>=' | '<=' | undefined;
|
|
5
|
+
export declare const isVulnKind: (value?: string) => value is VulnKinds;
|
|
6
|
+
export declare const asVulnKind: (value?: string) => VulnKinds;
|
|
7
|
+
export declare const parseInternals: (nodes: PostcssNode[]) => {
|
|
8
|
+
kind: VulnKinds;
|
|
9
|
+
comparator: VulnComparator;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* :vuln / :vulnerable Pseudo-Selector, matches nodes with vulnerability alerts.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* - :vuln - matches vulnerabilities with severity >= medium (critical, high, medium but not low)
|
|
16
|
+
* - :vuln(critical) - matches specific vulnerability kind
|
|
17
|
+
* - :vuln(>1) - matches vulnerabilities with severity greater than 1
|
|
18
|
+
* - :vuln(">=medium") - matches vulnerabilities with severity >= medium
|
|
19
|
+
*/
|
|
20
|
+
export declare const vuln: (state: ParserState) => Promise<ParserState & {
|
|
21
|
+
securityArchive: NonNullable<ParserState["securityArchive"]>;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { error } from '@vltpkg/error-cause';
|
|
2
|
+
import { asPostcssNodeWithChildren, asStringNode, asTagNode, isStringNode, isTagNode, } from '@vltpkg/dss-parser';
|
|
3
|
+
import { assertSecurityArchive, removeDanglingEdges, removeNode, removeQuotes, } from "./helpers.js";
|
|
4
|
+
// Valid vulnerability kind names
|
|
5
|
+
const kinds = new Set([
|
|
6
|
+
'critical',
|
|
7
|
+
'high',
|
|
8
|
+
'medium',
|
|
9
|
+
'low',
|
|
10
|
+
'0',
|
|
11
|
+
'1',
|
|
12
|
+
'2',
|
|
13
|
+
'3',
|
|
14
|
+
]);
|
|
15
|
+
// Map numerical values to their respective kinds for comparison operations
|
|
16
|
+
const kindLevelMap = new Map([
|
|
17
|
+
['critical', 0],
|
|
18
|
+
['high', 1],
|
|
19
|
+
['medium', 2],
|
|
20
|
+
['low', 3],
|
|
21
|
+
['0', 0],
|
|
22
|
+
['1', 1],
|
|
23
|
+
['2', 2],
|
|
24
|
+
['3', 3],
|
|
25
|
+
]);
|
|
26
|
+
// Maps alert types to their severity level for comparison operations.
|
|
27
|
+
// gptSecurity maps to medium (2), gptAnomaly maps to low (3).
|
|
28
|
+
const alertLevelMap = new Map([
|
|
29
|
+
['criticalCVE', 0],
|
|
30
|
+
['cve', 1],
|
|
31
|
+
['potentialVulnerability', 2],
|
|
32
|
+
['gptSecurity', 2],
|
|
33
|
+
['mildCVE', 3],
|
|
34
|
+
['gptAnomaly', 3],
|
|
35
|
+
]);
|
|
36
|
+
// Maps alert.severity strings to numeric levels for cveId-bearing alerts
|
|
37
|
+
// whose type may not be in alertLevelMap.
|
|
38
|
+
const severityLevelMap = new Map([
|
|
39
|
+
['critical', 0],
|
|
40
|
+
['high', 1],
|
|
41
|
+
['medium', 2],
|
|
42
|
+
['low', 3],
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* Returns the severity level for an alert, considering both its type
|
|
46
|
+
* (via alertLevelMap) and its cveId + severity fields. Alerts with a
|
|
47
|
+
* cveId are treated as vulnerability alerts even if their type is not
|
|
48
|
+
* in the closed set.
|
|
49
|
+
*/
|
|
50
|
+
const getAlertLevel = (alert) => {
|
|
51
|
+
const level = alertLevelMap.get(alert.type);
|
|
52
|
+
if (level != null)
|
|
53
|
+
return level;
|
|
54
|
+
// Fall back to severity field for alerts carrying a CVE id
|
|
55
|
+
if (alert.props?.cveId && alert.severity) {
|
|
56
|
+
return severityLevelMap.get(alert.severity);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
};
|
|
60
|
+
export const isVulnKind = (value) => kinds.has(value);
|
|
61
|
+
export const asVulnKind = (value) => {
|
|
62
|
+
if (!isVulnKind(value)) {
|
|
63
|
+
throw error('Expected a valid vuln kind', {
|
|
64
|
+
found: value,
|
|
65
|
+
validOptions: Array.from(kinds),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
export const parseInternals = (nodes) => {
|
|
71
|
+
// Handle case where no parameters are provided (parameterless :vuln)
|
|
72
|
+
if (!nodes[0]) {
|
|
73
|
+
return { kind: undefined, comparator: undefined };
|
|
74
|
+
}
|
|
75
|
+
const selectorNode = asPostcssNodeWithChildren(nodes[0]);
|
|
76
|
+
if (!selectorNode.nodes[0]) {
|
|
77
|
+
return { kind: undefined, comparator: undefined };
|
|
78
|
+
}
|
|
79
|
+
let kindValue = '';
|
|
80
|
+
let comparator = undefined;
|
|
81
|
+
let kind;
|
|
82
|
+
// Parse the parameter (kind with optional comparator)
|
|
83
|
+
if (isStringNode(selectorNode.nodes[0])) {
|
|
84
|
+
kindValue = removeQuotes(asStringNode(selectorNode.nodes[0]).value);
|
|
85
|
+
}
|
|
86
|
+
else if (isTagNode(selectorNode.nodes[0])) {
|
|
87
|
+
kindValue = asTagNode(selectorNode.nodes[0]).value;
|
|
88
|
+
}
|
|
89
|
+
// Extract comparator if present
|
|
90
|
+
if (kindValue.startsWith('>=')) {
|
|
91
|
+
comparator = '>=';
|
|
92
|
+
kindValue = kindValue.substring(2);
|
|
93
|
+
}
|
|
94
|
+
else if (kindValue.startsWith('<=')) {
|
|
95
|
+
comparator = '<=';
|
|
96
|
+
kindValue = kindValue.substring(2);
|
|
97
|
+
}
|
|
98
|
+
else if (kindValue.startsWith('>')) {
|
|
99
|
+
comparator = '>';
|
|
100
|
+
kindValue = kindValue.substring(1);
|
|
101
|
+
}
|
|
102
|
+
else if (kindValue.startsWith('<')) {
|
|
103
|
+
comparator = '<';
|
|
104
|
+
kindValue = kindValue.substring(1);
|
|
105
|
+
}
|
|
106
|
+
// Validate the kind without comparator
|
|
107
|
+
if (!comparator) {
|
|
108
|
+
kind = asVulnKind(kindValue);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// For comparisons, just make sure it's a valid numeric value or a valid kind
|
|
112
|
+
if (isVulnKind(kindValue)) {
|
|
113
|
+
kind = kindValue;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw error('Expected a valid vuln kind or number between 0-3', {
|
|
117
|
+
found: kindValue,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { kind, comparator };
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* :vuln / :vulnerable Pseudo-Selector, matches nodes with vulnerability alerts.
|
|
125
|
+
*
|
|
126
|
+
* Usage:
|
|
127
|
+
* - :vuln - matches vulnerabilities with severity >= medium (critical, high, medium but not low)
|
|
128
|
+
* - :vuln(critical) - matches specific vulnerability kind
|
|
129
|
+
* - :vuln(>1) - matches vulnerabilities with severity greater than 1
|
|
130
|
+
* - :vuln(">=medium") - matches vulnerabilities with severity >= medium
|
|
131
|
+
*/
|
|
132
|
+
export const vuln = async (state) => {
|
|
133
|
+
assertSecurityArchive(state, 'vuln');
|
|
134
|
+
let internals;
|
|
135
|
+
try {
|
|
136
|
+
internals = parseInternals(asPostcssNodeWithChildren(state.current).nodes);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
throw error('Failed to parse :vuln selector', { cause: err });
|
|
140
|
+
}
|
|
141
|
+
const { kind, comparator } = internals;
|
|
142
|
+
for (const node of state.partial.nodes) {
|
|
143
|
+
const report = state.securityArchive.get(node.id);
|
|
144
|
+
// Always exclude nodes that don't have security data or alerts
|
|
145
|
+
if (!report?.alerts || report.alerts.length === 0) {
|
|
146
|
+
removeNode(state, node);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const node of state.partial.nodes) {
|
|
150
|
+
const report = state.securityArchive.get(node.id);
|
|
151
|
+
let exclude = true;
|
|
152
|
+
if (report) {
|
|
153
|
+
if (kind === undefined && comparator === undefined) {
|
|
154
|
+
// Parameterless :vuln - match vuln alerts with severity >= medium
|
|
155
|
+
// Includes: criticalCVE, cve, potentialVulnerability, gptSecurity,
|
|
156
|
+
// and any alert carrying a cveId prop
|
|
157
|
+
// Excludes: mildCVE, gptAnomaly (low severity)
|
|
158
|
+
exclude = !report.alerts.some(alert => {
|
|
159
|
+
const level = getAlertLevel(alert);
|
|
160
|
+
return level != null && level <= 2;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else if (comparator) {
|
|
164
|
+
// retrieve the value to compare against
|
|
165
|
+
const kindLevel = kindLevelMap.get(kind);
|
|
166
|
+
// the kindLevel value has already been validated at this point
|
|
167
|
+
// and thus can never return an undefined/falsy value but ts doesn't
|
|
168
|
+
// know about that, so we have the extra check here
|
|
169
|
+
/* c8 ignore next - impossible */
|
|
170
|
+
if (kindLevel == null)
|
|
171
|
+
break;
|
|
172
|
+
// Check each alert to find any that match our comparison criteria
|
|
173
|
+
for (const alert of report.alerts) {
|
|
174
|
+
const currentAlertLevel = getAlertLevel(alert);
|
|
175
|
+
if (currentAlertLevel == null)
|
|
176
|
+
continue;
|
|
177
|
+
switch (comparator) {
|
|
178
|
+
case '>':
|
|
179
|
+
if (currentAlertLevel > kindLevel) {
|
|
180
|
+
exclude = false;
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
case '<':
|
|
184
|
+
if (currentAlertLevel < kindLevel) {
|
|
185
|
+
exclude = false;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case '>=':
|
|
189
|
+
if (currentAlertLevel >= kindLevel) {
|
|
190
|
+
exclude = false;
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case '<=':
|
|
194
|
+
if (currentAlertLevel <= kindLevel) {
|
|
195
|
+
exclude = false;
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
// If we've found a match, no need to check other alerts
|
|
200
|
+
if (!exclude)
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Exact match behavior — also matches gptSecurity/gptAnomaly
|
|
206
|
+
// at the same level as their mapped kind, and cveId-bearing alerts
|
|
207
|
+
const targetLevel = kindLevelMap.get(kind);
|
|
208
|
+
exclude = !report.alerts.some(alert => {
|
|
209
|
+
const level = getAlertLevel(alert);
|
|
210
|
+
return level != null && level === targetLevel;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (exclude) {
|
|
215
|
+
removeNode(state, node);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
removeDanglingEdges(state);
|
|
219
|
+
return state;
|
|
220
|
+
};
|
package/dist/pseudo.js
CHANGED
|
@@ -58,7 +58,7 @@ import { unknown } from "./pseudo/unknown.js";
|
|
|
58
58
|
import { unmaintained } from "./pseudo/unmaintained.js";
|
|
59
59
|
import { unpopular } from "./pseudo/unpopular.js";
|
|
60
60
|
import { unstable } from "./pseudo/unstable.js";
|
|
61
|
-
import {
|
|
61
|
+
import { vuln } from "./pseudo/vuln.js";
|
|
62
62
|
import { workspace } from "./pseudo/workspace.js";
|
|
63
63
|
/**
|
|
64
64
|
* :has Pseudo-Selector, matches only nodes that have valid results
|
|
@@ -341,8 +341,8 @@ const pseudoSelectors = new Map(Object.entries({
|
|
|
341
341
|
unpopular,
|
|
342
342
|
unstable,
|
|
343
343
|
v: semver,
|
|
344
|
-
vuln
|
|
345
|
-
vulnerable,
|
|
344
|
+
vuln,
|
|
345
|
+
vulnerable: vuln,
|
|
346
346
|
workspace,
|
|
347
347
|
}));
|
|
348
348
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type Insights = {
|
|
|
75
75
|
eval?: boolean;
|
|
76
76
|
fs?: boolean;
|
|
77
77
|
license?: LicenseInsights;
|
|
78
|
-
malware?:
|
|
78
|
+
malware?: boolean;
|
|
79
79
|
minified?: boolean;
|
|
80
80
|
native?: boolean;
|
|
81
81
|
network?: boolean;
|
|
@@ -95,6 +95,7 @@ export type Insights = {
|
|
|
95
95
|
unmaintained?: boolean;
|
|
96
96
|
unpopular?: boolean;
|
|
97
97
|
unstable?: boolean;
|
|
98
|
+
vuln?: VulnInsights;
|
|
98
99
|
};
|
|
99
100
|
export type LicenseInsights = {
|
|
100
101
|
unlicensed: boolean;
|
|
@@ -112,8 +113,8 @@ export type LeveledInsights = {
|
|
|
112
113
|
high: boolean;
|
|
113
114
|
critical: boolean;
|
|
114
115
|
};
|
|
115
|
-
export type MalwareInsights = LeveledInsights;
|
|
116
116
|
export type SeverityInsights = LeveledInsights;
|
|
117
|
+
export type VulnInsights = LeveledInsights;
|
|
117
118
|
export type SquatInsights = {
|
|
118
119
|
medium: boolean;
|
|
119
120
|
critical: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vltpkg/query",
|
|
3
3
|
"description": "Query syntax parser that retrieves items from a graph",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.34",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"url": "http://vlt.sh"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@vltpkg/dep-id": "1.0.0-rc.
|
|
17
|
-
"@vltpkg/dss-parser": "1.0.0-rc.
|
|
18
|
-
"@vltpkg/error-cause": "1.0.0-rc.
|
|
19
|
-
"@vltpkg/security-archive": "1.0.0-rc.
|
|
20
|
-
"@vltpkg/semver": "1.0.0-rc.
|
|
21
|
-
"@vltpkg/types": "1.0.0-rc.
|
|
16
|
+
"@vltpkg/dep-id": "1.0.0-rc.34",
|
|
17
|
+
"@vltpkg/dss-parser": "1.0.0-rc.34",
|
|
18
|
+
"@vltpkg/error-cause": "1.0.0-rc.34",
|
|
19
|
+
"@vltpkg/security-archive": "1.0.0-rc.34",
|
|
20
|
+
"@vltpkg/semver": "1.0.0-rc.34",
|
|
21
|
+
"@vltpkg/types": "1.0.0-rc.34",
|
|
22
22
|
"minimatch": "^10.1.1",
|
|
23
23
|
"p-retry": "^7.1.1",
|
|
24
24
|
"postcss-selector-parser": "^7.1.1"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@eslint/js": "^9.39.1",
|
|
28
28
|
"@types/node": "^22.19.2",
|
|
29
|
-
"@vltpkg/spec": "1.0.0-rc.
|
|
29
|
+
"@vltpkg/spec": "1.0.0-rc.34",
|
|
30
30
|
"eslint": "^9.39.1",
|
|
31
31
|
"prettier": "^3.7.4",
|
|
32
32
|
"tap": "^21.5.0",
|
|
@@ -88,10 +88,14 @@ Package properties:
|
|
|
88
88
|
Severity args accept names or numbers (`critical`/`0`, `high`/`1`,
|
|
89
89
|
`medium`/`2`, `low`/`3`) and comparators: `:severity(">=medium")`.
|
|
90
90
|
|
|
91
|
-
Threats: `:malware(
|
|
91
|
+
Threats: `:malware` (binary, no params), `:squat(sev?)`,
|
|
92
|
+
`:suspicious`, `:confused`.
|
|
92
93
|
|
|
93
|
-
Vulnerabilities: `:
|
|
94
|
-
`:cve(*)`, `:cwe(CWE-79)`, `:severity(level)`.
|
|
94
|
+
Vulnerabilities: `:vuln(sev?)` (alias `:vulnerable`),
|
|
95
|
+
`:cve(CVE-2023-1234)`, `:cve(*)`, `:cwe(CWE-79)`, `:severity(level)`.
|
|
96
|
+
`:vuln` without params matches severity ≥ medium; `:vuln(critical)`
|
|
97
|
+
exact-matches; `:vuln(">=high")` uses comparators. Also matches alerts
|
|
98
|
+
carrying a `cveId` prop.
|
|
95
99
|
|
|
96
100
|
Licensing: `:license(type)` — types: `unlicensed`, `misc`,
|
|
97
101
|
`restricted`, `ambiguous`, `copyleft`, `unknown`, `none`, `exception`.
|
|
@@ -112,7 +116,7 @@ kinds: `overall` (default), `license`, `maintenance`, `quality`,
|
|
|
112
116
|
## Audit query starters
|
|
113
117
|
|
|
114
118
|
```bash
|
|
115
|
-
vlt query ':malware(critical), :cve(*), :obfuscated' # critical issues
|
|
119
|
+
vlt query ':malware, :vuln(critical), :cve(*), :obfuscated' # critical issues
|
|
116
120
|
vlt query ':abandoned, :unmaintained, :unknown' # supply chain risk
|
|
117
121
|
vlt query ':license(copyleft), :license(unlicensed)' # license compliance
|
|
118
122
|
vlt query ':eval, :shell, :network, :fs' # behavior audit
|
|
@@ -143,17 +143,17 @@ so the dependency graph doubles as a security scanner. Selectors group
|
|
|
143
143
|
into four families — compose them with anchors and combinators like
|
|
144
144
|
any other filter:
|
|
145
145
|
|
|
146
|
-
| Family | Selectors
|
|
147
|
-
| --------------- |
|
|
148
|
-
| Threats | `:malware
|
|
149
|
-
| Vulnerabilities | `:cve(CVE-…)` `:cve(*)` `:cwe(CWE-79)` `:severity(">=medium")`
|
|
150
|
-
| Capabilities | `:eval` `:network` `:fs` `:shell` `:env`
|
|
151
|
-
| Hygiene | `:abandoned` `:unmaintained` `:deprecated` `:score("<=0.5", "maintenance")`
|
|
146
|
+
| Family | Selectors | Ask |
|
|
147
|
+
| --------------- | -------------------------------------------------------------------------------- | -------------------------------------- |
|
|
148
|
+
| Threats | `:malware` `:squat` `:obfuscated` `:suspicious` | is anything actively hostile? |
|
|
149
|
+
| Vulnerabilities | `:vuln(critical)` `:cve(CVE-…)` `:cve(*)` `:cwe(CWE-79)` `:severity(">=medium")` | known CVEs / vulns, filter by severity |
|
|
150
|
+
| Capabilities | `:eval` `:network` `:fs` `:shell` `:env` | what _can_ this code do? |
|
|
151
|
+
| Hygiene | `:abandoned` `:unmaintained` `:deprecated` `:score("<=0.5", "maintenance")` | will this rot on us? |
|
|
152
152
|
|
|
153
153
|
Audit recipes:
|
|
154
154
|
|
|
155
155
|
```bash
|
|
156
|
-
vlt query ':malware(critical), :squat, :obfuscated' # supply-chain sweep
|
|
156
|
+
vlt query ':malware, :vuln(critical), :squat, :obfuscated' # supply-chain sweep
|
|
157
157
|
vlt query ':workspace > :prod:severity(">=high")' # release blockers
|
|
158
158
|
vlt query ':dev:shell, :dev:network' # dev deps that spawn/phone home
|
|
159
159
|
```
|