@vltpkg/query 1.0.0-rc.32 → 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/hostname.js +3 -1
- package/dist/pseudo/malware.d.ts +3 -16
- package/dist/pseudo/malware.js +11 -171
- package/dist/pseudo/scripts.js +1 -1
- 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 +19 -10
- package/skills/dss-query/REFERENCE.md +123 -0
- package/skills/dss-query/SKILL.md +190 -0
- package/skills/dss-query/evals/README.md +79 -0
- package/skills/dss-query/evals/evals.json +89 -0
- package/skills/dss-query/evals/grade.mjs +480 -0
- package/src/attribute.ts +186 -0
- package/src/combinator.ts +132 -0
- package/src/id.ts +46 -0
- package/src/index.ts +620 -0
- package/src/parser.ts +122 -0
- package/src/pseudo/abandoned.ts +9 -0
- package/src/pseudo/attr.ts +92 -0
- package/src/pseudo/built.ts +19 -0
- package/src/pseudo/confused.ts +29 -0
- package/src/pseudo/cve.ts +93 -0
- package/src/pseudo/cwe.ts +97 -0
- package/src/pseudo/debug.ts +9 -0
- package/src/pseudo/deprecated.ts +9 -0
- package/src/pseudo/dev.ts +18 -0
- package/src/pseudo/diff.ts +90 -0
- package/src/pseudo/dist.ts +144 -0
- package/src/pseudo/dynamic.ts +9 -0
- package/src/pseudo/empty.ts +15 -0
- package/src/pseudo/entropic.ts +9 -0
- package/src/pseudo/env.ts +6 -0
- package/src/pseudo/eval.ts +9 -0
- package/src/pseudo/fs.ts +9 -0
- package/src/pseudo/helpers.ts +106 -0
- package/src/pseudo/host.ts +111 -0
- package/src/pseudo/hostname.ts +156 -0
- package/src/pseudo/license.ts +134 -0
- package/src/pseudo/link.ts +30 -0
- package/src/pseudo/malware.ts +41 -0
- package/src/pseudo/minified.ts +9 -0
- package/src/pseudo/missing.ts +16 -0
- package/src/pseudo/native.ts +9 -0
- package/src/pseudo/network.ts +9 -0
- package/src/pseudo/obfuscated.ts +9 -0
- package/src/pseudo/optional.ts +18 -0
- package/src/pseudo/outdated.ts +305 -0
- package/src/pseudo/overridden.ts +20 -0
- package/src/pseudo/path.ts +146 -0
- package/src/pseudo/peer.ts +18 -0
- package/src/pseudo/prerelease.ts +47 -0
- package/src/pseudo/private.ts +19 -0
- package/src/pseudo/prod.ts +18 -0
- package/src/pseudo/published.ts +248 -0
- package/src/pseudo/registry.ts +29 -0
- package/src/pseudo/root.ts +19 -0
- package/src/pseudo/scanned.ts +20 -0
- package/src/pseudo/score.ts +186 -0
- package/src/pseudo/scripts.ts +54 -0
- package/src/pseudo/semver.ts +320 -0
- package/src/pseudo/severity.ts +231 -0
- package/src/pseudo/shell.ts +9 -0
- package/src/pseudo/shrinkwrap.ts +9 -0
- package/src/pseudo/spec.ts +131 -0
- package/src/pseudo/squat.ts +225 -0
- package/src/pseudo/suspicious.ts +9 -0
- package/src/pseudo/tracker.ts +9 -0
- package/src/pseudo/trivial.ts +9 -0
- package/src/pseudo/type.ts +26 -0
- package/src/pseudo/undesirable.ts +9 -0
- package/src/pseudo/unknown.ts +9 -0
- package/src/pseudo/unmaintained.ts +9 -0
- package/src/pseudo/unpopular.ts +9 -0
- package/src/pseudo/unstable.ts +9 -0
- package/src/pseudo/vuln.ts +273 -0
- package/src/pseudo/workspace.ts +22 -0
- package/src/pseudo.ts +413 -0
- package/src/types.ts +165 -0
- package/tap-snapshots/test/attribute.ts.test.cjs +304 -0
- package/tap-snapshots/test/combinator.ts.test.cjs +333 -0
- package/tap-snapshots/test/id.ts.test.cjs +118 -0
- package/tap-snapshots/test/parser.ts.test.cjs +344 -0
- package/tap-snapshots/test/pseudo/abandoned.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/attr.ts.test.cjs +161 -0
- package/tap-snapshots/test/pseudo/built.ts.test.cjs +70 -0
- package/tap-snapshots/test/pseudo/confused.ts.test.cjs +32 -0
- package/tap-snapshots/test/pseudo/cve.ts.test.cjs +49 -0
- package/tap-snapshots/test/pseudo/cwe.ts.test.cjs +49 -0
- package/tap-snapshots/test/pseudo/debug.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/deprecated.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/dist.ts.test.cjs +30 -0
- package/tap-snapshots/test/pseudo/dynamic.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/empty.ts.test.cjs +50 -0
- package/tap-snapshots/test/pseudo/entropic.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/env.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/eval.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/fs.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/helpers.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/hostname.ts.test.cjs +148 -0
- package/tap-snapshots/test/pseudo/license.ts.test.cjs +41 -0
- package/tap-snapshots/test/pseudo/link.ts.test.cjs +36 -0
- package/tap-snapshots/test/pseudo/malware.ts.test.cjs +22 -0
- package/tap-snapshots/test/pseudo/minified.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/missing.ts.test.cjs +30 -0
- package/tap-snapshots/test/pseudo/native.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/network.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/obfuscated.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/outdated.ts.test.cjs +130 -0
- package/tap-snapshots/test/pseudo/overridden.ts.test.cjs +75 -0
- package/tap-snapshots/test/pseudo/prerelease.ts.test.cjs +101 -0
- package/tap-snapshots/test/pseudo/private.ts.test.cjs +38 -0
- package/tap-snapshots/test/pseudo/published.ts.test.cjs +171 -0
- package/tap-snapshots/test/pseudo/registry.ts.test.cjs +68 -0
- package/tap-snapshots/test/pseudo/root.ts.test.cjs +24 -0
- package/tap-snapshots/test/pseudo/scanned.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/score.ts.test.cjs +239 -0
- package/tap-snapshots/test/pseudo/scripts.ts.test.cjs +61 -0
- package/tap-snapshots/test/pseudo/semver.ts.test.cjs +575 -0
- package/tap-snapshots/test/pseudo/severity.ts.test.cjs +95 -0
- package/tap-snapshots/test/pseudo/shell.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/shrinkwrap.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/spec.ts.test.cjs +148 -0
- package/tap-snapshots/test/pseudo/squat.ts.test.cjs +129 -0
- package/tap-snapshots/test/pseudo/suspicious.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/tracker.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/trivial.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/type.ts.test.cjs +51 -0
- package/tap-snapshots/test/pseudo/undesirable.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unknown.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unmaintained.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unpopular.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/unstable.ts.test.cjs +18 -0
- package/tap-snapshots/test/pseudo/vuln.ts.test.cjs +208 -0
- package/tap-snapshots/test/pseudo/vulnerable.ts.test.cjs +20 -0
- package/tap-snapshots/test/pseudo/workspace.ts.test.cjs +19 -0
- package/tap-snapshots/test/pseudo.ts.test.cjs +1247 -0
- package/test/attribute.ts +266 -0
- package/test/combinator.ts +168 -0
- package/test/fixtures/graph.ts +999 -0
- package/test/fixtures/selector.ts +103 -0
- package/test/fixtures/types.ts +7 -0
- package/test/id.ts +102 -0
- package/test/index.ts +1000 -0
- package/test/parser.ts +117 -0
- package/test/pseudo/abandoned.ts +98 -0
- package/test/pseudo/attr.ts +298 -0
- package/test/pseudo/built.ts +214 -0
- package/test/pseudo/confused.ts +160 -0
- package/test/pseudo/cve.ts +249 -0
- package/test/pseudo/cwe.ts +255 -0
- package/test/pseudo/debug.ts +98 -0
- package/test/pseudo/deprecated.ts +98 -0
- package/test/pseudo/dev.ts +61 -0
- package/test/pseudo/diff.ts +417 -0
- package/test/pseudo/dist.ts +215 -0
- package/test/pseudo/dynamic.ts +98 -0
- package/test/pseudo/empty.ts +108 -0
- package/test/pseudo/entropic.ts +101 -0
- package/test/pseudo/env.ts +98 -0
- package/test/pseudo/eval.ts +98 -0
- package/test/pseudo/fs.ts +98 -0
- package/test/pseudo/helpers.ts +370 -0
- package/test/pseudo/host-context.ts +276 -0
- package/test/pseudo/hostname.ts +314 -0
- package/test/pseudo/license.ts +265 -0
- package/test/pseudo/link.ts +94 -0
- package/test/pseudo/malware.ts +184 -0
- package/test/pseudo/minified.ts +98 -0
- package/test/pseudo/missing.ts +111 -0
- package/test/pseudo/native.ts +98 -0
- package/test/pseudo/network.ts +98 -0
- package/test/pseudo/obfuscated.ts +98 -0
- package/test/pseudo/optional.ts +73 -0
- package/test/pseudo/outdated.ts +331 -0
- package/test/pseudo/overridden.ts +317 -0
- package/test/pseudo/path.ts +680 -0
- package/test/pseudo/peer.ts +101 -0
- package/test/pseudo/prerelease.ts +279 -0
- package/test/pseudo/private.ts +108 -0
- package/test/pseudo/prod.ts +61 -0
- package/test/pseudo/published.ts +557 -0
- package/test/pseudo/registry.ts +122 -0
- package/test/pseudo/root.ts +66 -0
- package/test/pseudo/scanned.ts +78 -0
- package/test/pseudo/score.ts +591 -0
- package/test/pseudo/scripts.ts +294 -0
- package/test/pseudo/semver.ts +822 -0
- package/test/pseudo/severity.ts +310 -0
- package/test/pseudo/shell.ts +98 -0
- package/test/pseudo/shrinkwrap.ts +98 -0
- package/test/pseudo/spec.ts +525 -0
- package/test/pseudo/squat.ts +565 -0
- package/test/pseudo/suspicious.ts +98 -0
- package/test/pseudo/tracker.ts +98 -0
- package/test/pseudo/trivial.ts +98 -0
- package/test/pseudo/type.ts +105 -0
- package/test/pseudo/undesirable.ts +98 -0
- package/test/pseudo/unknown.ts +98 -0
- package/test/pseudo/unmaintained.ts +98 -0
- package/test/pseudo/unpopular.ts +98 -0
- package/test/pseudo/unstable.ts +98 -0
- package/test/pseudo/vuln.ts +625 -0
- package/test/pseudo/vulnerable.ts +137 -0
- package/test/pseudo/workspace.ts +111 -0
- package/test/pseudo.ts +558 -0
- package/tsconfig.json +3 -0
- package/tsconfig.publish.json +19 -0
- package/tsconfig.publish.tsbuildinfo +1 -0
- package/typedoc.mjs +2 -0
- package/dist/pseudo/vulnerable.d.ts +0 -8
- package/dist/pseudo/vulnerable.js +0 -17
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic grader for dss-query skill evals.
|
|
3
|
+
* Run from src/query (so @vltpkg/* workspace deps resolve):
|
|
4
|
+
* node skills/dss-query/evals/grade.mjs skills/dss-query-workspace/iteration-1
|
|
5
|
+
*
|
|
6
|
+
* Writes grading.json ({expectations: [{text, passed, evidence}]}) into each
|
|
7
|
+
* <eval>/<variant>/ directory.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
} from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { parse } from '@vltpkg/dss-parser'
|
|
17
|
+
import { Query } from '../../../src/index.ts'
|
|
18
|
+
import { getSimpleGraph } from '../../../test/fixtures/graph.ts'
|
|
19
|
+
|
|
20
|
+
const iterDir = process.argv[2]
|
|
21
|
+
if (!iterDir) throw new Error('usage: node grade.mjs <iteration-dir>')
|
|
22
|
+
|
|
23
|
+
// ---- selector extraction -------------------------------------------------
|
|
24
|
+
const extractSelectors = text => {
|
|
25
|
+
const out = new Set()
|
|
26
|
+
// vlt query '<sel>' / "<sel>" anywhere in the text
|
|
27
|
+
for (const m of text.matchAll(/vlt query '([^']+)'/g)) out.add(m[1])
|
|
28
|
+
for (const m of text.matchAll(/vlt query "([^"]+)"/g)) out.add(m[1])
|
|
29
|
+
// bare selectors on their own line inside fenced code blocks
|
|
30
|
+
for (const block of text.matchAll(/```[a-z]*\n([\s\S]*?)```/g)) {
|
|
31
|
+
for (const line of block[1].split('\n')) {
|
|
32
|
+
const t = line.trim()
|
|
33
|
+
// '# ...' is a comment, not an ID selector (real IDs: '#react', no space)
|
|
34
|
+
if (/^# /.test(t)) continue
|
|
35
|
+
if (/^[:#[*]/.test(t) && !t.includes('<')) out.add(t)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...out].filter(s => !s.includes('<'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- execution (offline only) ---------------------------------------------
|
|
42
|
+
// :outdated fetches registry packuments; security selectors need the archive.
|
|
43
|
+
const NO_EXEC = /:outdated|:missing/
|
|
44
|
+
const skipExec = s => Query.hasSecuritySelectors(s) || NO_EXEC.test(s)
|
|
45
|
+
// split top-level commas so loose mode can't mask invalid segments
|
|
46
|
+
const topLevelSplit = s => {
|
|
47
|
+
const parts = []
|
|
48
|
+
let depth = 0,
|
|
49
|
+
cur = ''
|
|
50
|
+
for (const ch of s) {
|
|
51
|
+
if (ch === '(' || ch === '[') depth++
|
|
52
|
+
else if (ch === ')' || ch === ']') depth--
|
|
53
|
+
if (ch === ',' && depth === 0) {
|
|
54
|
+
parts.push(cur)
|
|
55
|
+
cur = ''
|
|
56
|
+
} else cur += ch
|
|
57
|
+
}
|
|
58
|
+
parts.push(cur)
|
|
59
|
+
return parts.map(p => p.trim()).filter(Boolean)
|
|
60
|
+
}
|
|
61
|
+
const mockSignal = { throwIfAborted: () => {} }
|
|
62
|
+
const makeQuery = () => {
|
|
63
|
+
const graph = getSimpleGraph()
|
|
64
|
+
return new Query({
|
|
65
|
+
nodes: new Set(graph.nodes.values()),
|
|
66
|
+
edges: graph.edges,
|
|
67
|
+
importers: graph.importers,
|
|
68
|
+
securityArchive: undefined,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parseAll = selectors => {
|
|
73
|
+
const failures = []
|
|
74
|
+
for (const s of selectors) {
|
|
75
|
+
try {
|
|
76
|
+
parse(s)
|
|
77
|
+
} catch (err) {
|
|
78
|
+
failures.push(`${s} → ${err.message}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return failures
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const execAll = async selectors => {
|
|
85
|
+
const failures = []
|
|
86
|
+
const skipped = []
|
|
87
|
+
for (const s of selectors) {
|
|
88
|
+
if (skipExec(s)) {
|
|
89
|
+
skipped.push(s)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
for (const seg of topLevelSplit(s)) {
|
|
93
|
+
try {
|
|
94
|
+
await makeQuery().search(seg, { signal: mockSignal })
|
|
95
|
+
} catch (err) {
|
|
96
|
+
failures.push(`${seg} → ${err.message}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { failures, skipped }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- per-eval assertion logic ----------------------------------------------
|
|
104
|
+
const first = (arr, re) => arr.find(s => re.test(s))
|
|
105
|
+
const preCodeText = text =>
|
|
106
|
+
text.slice(0, Math.max(text.indexOf('```'), 0)).trim()
|
|
107
|
+
|
|
108
|
+
const graders = {
|
|
109
|
+
0: async (text, sels) => {
|
|
110
|
+
const hit = first(sels, /:root\s*>\s*\*?:outdated/)
|
|
111
|
+
const parseFails = parseAll(sels)
|
|
112
|
+
const { failures: execFails, skipped } = await execAll(sels)
|
|
113
|
+
const pre = preCodeText(text)
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
text: 'proposes-root-outdated-selector',
|
|
117
|
+
passed: !!hit,
|
|
118
|
+
evidence: hit ?? `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
text: 'all-selectors-parse',
|
|
122
|
+
passed: parseFails.length === 0,
|
|
123
|
+
evidence:
|
|
124
|
+
parseFails.join('; ') || `${sels.length} selectors parsed`,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
text: 'selector-executes-on-fixture-graph',
|
|
128
|
+
passed: execFails.length === 0,
|
|
129
|
+
evidence:
|
|
130
|
+
execFails.join('; ') ||
|
|
131
|
+
`executed ${sels.length - skipped.length}, skipped (network-dependent): ${skipped.length}`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
text: 'query-first-style',
|
|
135
|
+
passed: pre.length < 150,
|
|
136
|
+
evidence:
|
|
137
|
+
pre.length < 150 ?
|
|
138
|
+
`only ${pre.length} chars before first code block`
|
|
139
|
+
: `${pre.length} chars of preamble before first code block: "${pre.slice(0, 80)}..."`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
text: 'mentions-view-flag',
|
|
143
|
+
passed: /--view/.test(text),
|
|
144
|
+
evidence:
|
|
145
|
+
/--view/.test(text) ?
|
|
146
|
+
`matched "${text.match(/--view\S*/)[0]}"`
|
|
147
|
+
: 'no --view mention',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
text: 'offers-related-queries',
|
|
151
|
+
passed: sels.length >= 2,
|
|
152
|
+
evidence: `${sels.length} distinct selectors offered`,
|
|
153
|
+
},
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
1: async (text, sels) => {
|
|
157
|
+
const attr = first(sels, /\[license\^?=\s*['"]?MIT/i)
|
|
158
|
+
const badPseudo = /:license\(\s*['"]?mit/i.test(text)
|
|
159
|
+
const anchored = first(sels, /:workspace\s*>\s*:prod/)
|
|
160
|
+
const parseFails = parseAll(sels)
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
text: 'uses-license-attribute-form',
|
|
164
|
+
passed: !!attr,
|
|
165
|
+
evidence:
|
|
166
|
+
attr ?? `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
text: 'avoids-invalid-license-pseudo',
|
|
170
|
+
passed: !badPseudo,
|
|
171
|
+
evidence:
|
|
172
|
+
badPseudo ?
|
|
173
|
+
'answer proposes :license(mit), which throws "Expected a valid license kind"'
|
|
174
|
+
: 'no :license(mit) present',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
text: 'workspace-prod-anchored',
|
|
178
|
+
passed: !!anchored,
|
|
179
|
+
evidence:
|
|
180
|
+
anchored ?? `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
text: 'all-selectors-parse',
|
|
184
|
+
passed: parseFails.length === 0,
|
|
185
|
+
evidence:
|
|
186
|
+
parseFails.join('; ') || `${sels.length} selectors parsed`,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
text: 'offers-related-queries',
|
|
190
|
+
passed: sels.length >= 2,
|
|
191
|
+
evidence: `${sels.length} distinct selectors offered`,
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
2: async (text, sels) => {
|
|
196
|
+
const inv = first(
|
|
197
|
+
sels,
|
|
198
|
+
/:has\(\s*>\s*(#react|\[name=react\])\s*\)/,
|
|
199
|
+
)
|
|
200
|
+
const correction =
|
|
201
|
+
/(#react\s*>\s*\*|wrong way|opposite direction|points? the wrong|selects? react'?s (own )?dependenc)/i.test(
|
|
202
|
+
text,
|
|
203
|
+
) && /dependenc/i.test(text)
|
|
204
|
+
// the recommended (first extracted) selector must not be the wrong-direction one
|
|
205
|
+
const firstSel = sels[0] ?? ''
|
|
206
|
+
const wrongAsAnswer = /^#react\s*>\s*\*$/.test(firstSel)
|
|
207
|
+
const parseFails = parseAll(sels)
|
|
208
|
+
const evidenceLine =
|
|
209
|
+
text
|
|
210
|
+
.split('\n')
|
|
211
|
+
.find(l => /wrong|opposite|dependenc/i.test(l)) ?? ''
|
|
212
|
+
return [
|
|
213
|
+
{
|
|
214
|
+
text: 'uses-has-inversion',
|
|
215
|
+
passed: !!inv,
|
|
216
|
+
evidence: inv ?? `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
text: 'corrects-direction-mistake',
|
|
220
|
+
passed: correction,
|
|
221
|
+
evidence:
|
|
222
|
+
evidenceLine.trim().slice(0, 160) ||
|
|
223
|
+
'no direction-correction language found',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
text: 'does-not-present-wrong-selector-as-answer',
|
|
227
|
+
passed: !wrongAsAnswer,
|
|
228
|
+
evidence: `first recommended selector: ${firstSel || '(none)'}`,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
text: 'all-selectors-parse',
|
|
232
|
+
passed: parseFails.length === 0,
|
|
233
|
+
evidence:
|
|
234
|
+
parseFails.join('; ') || `${sels.length} selectors parsed`,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
text: 'explains-combinator-rationale',
|
|
238
|
+
passed:
|
|
239
|
+
/point.{0,40}dependenc|dependent\s*(→|->|to)\s*dependenc|parent.{0,12}child/i.test(
|
|
240
|
+
text,
|
|
241
|
+
),
|
|
242
|
+
evidence: (
|
|
243
|
+
text.split('\n').find(l => /point|parent|→|->/.test(l)) ??
|
|
244
|
+
'no general direction rule stated'
|
|
245
|
+
)
|
|
246
|
+
.trim()
|
|
247
|
+
.slice(0, 160),
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
3: async text => {
|
|
252
|
+
const checks = [
|
|
253
|
+
['covers-workspace-anchor', /workspace/i],
|
|
254
|
+
['covers-direct-prod-segment', null], // special: needs both
|
|
255
|
+
['covers-cve-segment', /cve/i],
|
|
256
|
+
['mentions-security-data-source', /socket|network/i],
|
|
257
|
+
]
|
|
258
|
+
return checks.map(([name, re]) => {
|
|
259
|
+
if (name === 'covers-direct-prod-segment') {
|
|
260
|
+
const ok = /direct/i.test(text) && /prod/i.test(text)
|
|
261
|
+
return {
|
|
262
|
+
text: name,
|
|
263
|
+
passed: ok,
|
|
264
|
+
evidence:
|
|
265
|
+
ok ?
|
|
266
|
+
'mentions both "direct" and "prod"'
|
|
267
|
+
: `direct:${/direct/i.test(text)} prod:${/prod/i.test(text)}`,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const m = text.match(re)
|
|
271
|
+
return {
|
|
272
|
+
text: name,
|
|
273
|
+
passed: !!m,
|
|
274
|
+
evidence: m ? `matched "${m[0]}"` : `no match for ${re}`,
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
4: async (text, sels) => {
|
|
279
|
+
const threat =
|
|
280
|
+
/:malware/.test(text) &&
|
|
281
|
+
/:squat|:obfuscated|:suspicious/.test(text)
|
|
282
|
+
const parseFails = parseAll(sels)
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
text: 'uses-threat-selectors',
|
|
286
|
+
passed: threat,
|
|
287
|
+
evidence:
|
|
288
|
+
threat ?
|
|
289
|
+
`selectors: ${sels.join(' | ')}`
|
|
290
|
+
: `malware:${/:malware/.test(text)} squat/obf/susp:${/:squat|:obfuscated|:suspicious/.test(text)}`,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
text: 'all-selectors-parse',
|
|
294
|
+
passed: parseFails.length === 0,
|
|
295
|
+
evidence:
|
|
296
|
+
parseFails.join('; ') || `${sels.length} selectors parsed`,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
text: 'mentions-security-data-source',
|
|
300
|
+
passed: /socket|network/i.test(text),
|
|
301
|
+
evidence:
|
|
302
|
+
text.match(/socket|network/i)?.[0] ??
|
|
303
|
+
'no Socket/network mention',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
text: 'offers-related-queries',
|
|
307
|
+
passed: sels.length >= 2,
|
|
308
|
+
evidence: `${sels.length} distinct selectors offered`,
|
|
309
|
+
},
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
5: async (text, sels) => {
|
|
313
|
+
const cap =
|
|
314
|
+
/:dev[^\s,']*:(shell|network)|:dev:is\([^)]*:(shell|network)/.test(
|
|
315
|
+
text,
|
|
316
|
+
)
|
|
317
|
+
const both = /:shell/.test(text) && /:network/.test(text)
|
|
318
|
+
const parseFails = parseAll(sels)
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
text: 'uses-capability-selectors',
|
|
322
|
+
passed: cap,
|
|
323
|
+
evidence:
|
|
324
|
+
cap ?
|
|
325
|
+
`selectors: ${sels.join(' | ')}`
|
|
326
|
+
: `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
text: 'covers-both-capabilities',
|
|
330
|
+
passed: both,
|
|
331
|
+
evidence: `shell:${/:shell/.test(text)} network:${/:network/.test(text)}`,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
text: 'all-selectors-parse',
|
|
335
|
+
passed: parseFails.length === 0,
|
|
336
|
+
evidence:
|
|
337
|
+
parseFails.join('; ') || `${sels.length} selectors parsed`,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
text: 'explains-capability-meaning',
|
|
341
|
+
passed: /socket|static|capab|can\s/i.test(text),
|
|
342
|
+
evidence:
|
|
343
|
+
text.match(/socket|static|capab|can\s\S*/i)?.[0] ??
|
|
344
|
+
'no capability explanation',
|
|
345
|
+
},
|
|
346
|
+
]
|
|
347
|
+
},
|
|
348
|
+
6: async (text, sels) => {
|
|
349
|
+
const sev =
|
|
350
|
+
first(sels, /:sev(erity)?\(/) ??
|
|
351
|
+
(/:sev(erity)?\(/.test(text) ?
|
|
352
|
+
text.match(/:sev(erity)?\([^)]*\)/)[0]
|
|
353
|
+
: undefined)
|
|
354
|
+
const anchored = /:workspace\s*>\s*:prod/.test(text)
|
|
355
|
+
const parseFails = parseAll(sels)
|
|
356
|
+
return [
|
|
357
|
+
{
|
|
358
|
+
text: 'uses-severity-filter',
|
|
359
|
+
passed: !!sev,
|
|
360
|
+
evidence: sev ?? `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
text: 'anchors-workspace-prod',
|
|
364
|
+
passed: anchored,
|
|
365
|
+
evidence:
|
|
366
|
+
anchored ?
|
|
367
|
+
text.match(/:workspace\s*>\s*:prod\S*/)[0]
|
|
368
|
+
: `selectors: ${sels.join(' | ') || '(none)'}`,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
text: 'all-selectors-parse',
|
|
372
|
+
passed: parseFails.length === 0,
|
|
373
|
+
evidence:
|
|
374
|
+
parseFails.join('; ') || `${sels.length} selectors parsed`,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
text: 'mentions-security-data-source',
|
|
378
|
+
passed: /socket|network/i.test(text),
|
|
379
|
+
evidence:
|
|
380
|
+
text.match(/socket|network/i)?.[0] ??
|
|
381
|
+
'no Socket/network mention',
|
|
382
|
+
},
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---- round-3 style assertions (Examples tail, docs deep links, concision) ---
|
|
388
|
+
const examplesHeading = text => ({
|
|
389
|
+
text: 'uses-examples-heading',
|
|
390
|
+
passed: /Examples:/.test(text) && !/Related:/.test(text),
|
|
391
|
+
evidence: `Examples: ${/Examples:/.test(text)}, Related: ${/Related:/.test(text)}`,
|
|
392
|
+
})
|
|
393
|
+
const docsLink = re => text => ({
|
|
394
|
+
text: 'ends-with-docs-link',
|
|
395
|
+
passed: re.test(text),
|
|
396
|
+
evidence: text.match(re)?.[0] ?? `no link matching ${re}`,
|
|
397
|
+
})
|
|
398
|
+
const conciseAnswer = max => text => {
|
|
399
|
+
const lines = text.split('\n').filter(l => l.trim()).length
|
|
400
|
+
return {
|
|
401
|
+
text: 'concise-answer',
|
|
402
|
+
passed: lines <= max,
|
|
403
|
+
evidence: `${lines} non-empty lines (max ${max})`,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const SELECTORS_DOC = /docs\.vlt\.io\/cli\/selectors/
|
|
407
|
+
const round3Extras = {
|
|
408
|
+
0: [examplesHeading, docsLink(SELECTORS_DOC)],
|
|
409
|
+
1: [
|
|
410
|
+
examplesHeading,
|
|
411
|
+
// license query: either the structural selectors page or the
|
|
412
|
+
// license-compliance section is a correct deep link
|
|
413
|
+
docsLink(
|
|
414
|
+
/docs\.vlt\.io\/cli\/(selectors|security#license-compliance)/,
|
|
415
|
+
),
|
|
416
|
+
],
|
|
417
|
+
2: [docsLink(SELECTORS_DOC)],
|
|
418
|
+
3: [
|
|
419
|
+
docsLink(/docs\.vlt\.io\/cli\/security#vulnerability-detection/),
|
|
420
|
+
],
|
|
421
|
+
4: [docsLink(/docs\.vlt\.io\/cli\/security#malware-detection/)],
|
|
422
|
+
5: [
|
|
423
|
+
conciseAnswer(22),
|
|
424
|
+
docsLink(
|
|
425
|
+
/docs\.vlt\.io\/cli\/security#behavioral-security-risks/,
|
|
426
|
+
),
|
|
427
|
+
],
|
|
428
|
+
6: [
|
|
429
|
+
examplesHeading,
|
|
430
|
+
docsLink(/docs\.vlt\.io\/cli\/security#vulnerability-detection/),
|
|
431
|
+
],
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ---- run --------------------------------------------------------------------
|
|
435
|
+
for (const evalDir of readdirSync(iterDir).filter(d =>
|
|
436
|
+
d.startsWith('eval-'),
|
|
437
|
+
)) {
|
|
438
|
+
const id = Number(evalDir.match(/^eval-(\d+)/)[1])
|
|
439
|
+
const variants = readdirSync(join(iterDir, evalDir), {
|
|
440
|
+
withFileTypes: true,
|
|
441
|
+
})
|
|
442
|
+
.filter(d => d.isDirectory())
|
|
443
|
+
.map(d => d.name)
|
|
444
|
+
for (const variant of variants) {
|
|
445
|
+
const answerPath = join(
|
|
446
|
+
iterDir,
|
|
447
|
+
evalDir,
|
|
448
|
+
variant,
|
|
449
|
+
'run-1',
|
|
450
|
+
'outputs',
|
|
451
|
+
'answer.md',
|
|
452
|
+
)
|
|
453
|
+
if (!existsSync(answerPath)) continue
|
|
454
|
+
const text = readFileSync(answerPath, 'utf8')
|
|
455
|
+
const sels = extractSelectors(text)
|
|
456
|
+
const expectations = [
|
|
457
|
+
...(await graders[id](text, sels)),
|
|
458
|
+
...(round3Extras[id] ?? []).map(fn => fn(text)),
|
|
459
|
+
]
|
|
460
|
+
const passed = expectations.filter(e => e.passed).length
|
|
461
|
+
writeFileSync(
|
|
462
|
+
join(iterDir, evalDir, variant, 'run-1', 'grading.json'),
|
|
463
|
+
JSON.stringify(
|
|
464
|
+
{
|
|
465
|
+
summary: {
|
|
466
|
+
total: expectations.length,
|
|
467
|
+
passed,
|
|
468
|
+
pass_rate: passed / expectations.length,
|
|
469
|
+
},
|
|
470
|
+
expectations,
|
|
471
|
+
},
|
|
472
|
+
null,
|
|
473
|
+
2,
|
|
474
|
+
) + '\n',
|
|
475
|
+
)
|
|
476
|
+
console.log(
|
|
477
|
+
`${evalDir}/${variant}: ${passed}/${expectations.length}`,
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
}
|
package/src/attribute.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { asAttributeNode } from '@vltpkg/dss-parser'
|
|
2
|
+
import { error } from '@vltpkg/error-cause'
|
|
3
|
+
import { removeDanglingEdges } from './pseudo/helpers.ts'
|
|
4
|
+
import type { NodeLike, JSONField, Manifest } from '@vltpkg/types'
|
|
5
|
+
import type { ParserState } from './types.ts'
|
|
6
|
+
|
|
7
|
+
export type ComparatorFn = (attr: string, value?: string) => boolean
|
|
8
|
+
|
|
9
|
+
// JSONField has a mapped type constituent that would coerce to [object Object]
|
|
10
|
+
// when stringified, which is what we want in this case.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
12
|
+
const jsonFieldToString = (v: JSONField) => String(v)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve the {@link Manifest} values found at the given `properties`
|
|
16
|
+
* location for a given {@link Node}.
|
|
17
|
+
*/
|
|
18
|
+
export const getManifestPropertyValues = (
|
|
19
|
+
node: NodeLike,
|
|
20
|
+
properties: string[],
|
|
21
|
+
attribute: string,
|
|
22
|
+
): string[] | undefined => {
|
|
23
|
+
if (!node.manifest) return
|
|
24
|
+
|
|
25
|
+
const traverse = new Set<JSONField>([node.manifest])
|
|
26
|
+
const props = new Set<JSONField>()
|
|
27
|
+
for (const key of properties) {
|
|
28
|
+
for (const prop of traverse) {
|
|
29
|
+
/* c8 ignore start - should be impossible */
|
|
30
|
+
if (!prop) {
|
|
31
|
+
throw error('failed to find nested property in :attr', {
|
|
32
|
+
found: properties,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
/* c8 ignore stop */
|
|
36
|
+
|
|
37
|
+
// expand the result list to include nested array values
|
|
38
|
+
if (Array.isArray(prop)) {
|
|
39
|
+
for (const p of prop) {
|
|
40
|
+
traverse.add(p)
|
|
41
|
+
}
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// guard for inspecting keys of objects next
|
|
46
|
+
if (
|
|
47
|
+
typeof prop === 'string' ||
|
|
48
|
+
typeof prop === 'number' ||
|
|
49
|
+
typeof prop === 'boolean'
|
|
50
|
+
) {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// assign next value when found
|
|
55
|
+
if (key in prop) {
|
|
56
|
+
const nextValue = prop[key]
|
|
57
|
+
if (nextValue) {
|
|
58
|
+
if (key === attribute) {
|
|
59
|
+
props.add(nextValue)
|
|
60
|
+
} else {
|
|
61
|
+
traverse.delete(prop)
|
|
62
|
+
traverse.add(nextValue)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// if no value was found after trying a given key
|
|
69
|
+
// then there's nothing to be collected
|
|
70
|
+
if (!props.size) return
|
|
71
|
+
|
|
72
|
+
// expand the result to include array values
|
|
73
|
+
const collect = new Set<string>()
|
|
74
|
+
for (const prop of props) {
|
|
75
|
+
if (Array.isArray(prop)) {
|
|
76
|
+
for (const p of prop) {
|
|
77
|
+
collect.add(p ? jsonFieldToString(p) : '')
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
collect.add(jsonFieldToString(prop))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...collect]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// decorator style of function that will filter `ParserState` results
|
|
88
|
+
// based on a provided `comparator` function
|
|
89
|
+
export const filterAttributes = (
|
|
90
|
+
state: ParserState,
|
|
91
|
+
comparator: ComparatorFn | undefined,
|
|
92
|
+
value: string,
|
|
93
|
+
propertyName: string,
|
|
94
|
+
insensitive: boolean,
|
|
95
|
+
prefixProperties: string[] = [],
|
|
96
|
+
): ParserState => {
|
|
97
|
+
const check = (attr: JSONField) =>
|
|
98
|
+
comparator?.(
|
|
99
|
+
insensitive ?
|
|
100
|
+
jsonFieldToString(attr).toLowerCase()
|
|
101
|
+
: jsonFieldToString(attr),
|
|
102
|
+
insensitive ? value.toLowerCase() : value,
|
|
103
|
+
)
|
|
104
|
+
const deleteNode = (node: NodeLike) => {
|
|
105
|
+
for (const edge of node.edgesIn) {
|
|
106
|
+
state.partial.edges.delete(edge)
|
|
107
|
+
}
|
|
108
|
+
state.partial.nodes.delete(node)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const node of state.partial.nodes) {
|
|
112
|
+
const prefixes =
|
|
113
|
+
prefixProperties.length ? prefixProperties : [propertyName]
|
|
114
|
+
const attrs = getManifestPropertyValues(
|
|
115
|
+
node,
|
|
116
|
+
prefixes,
|
|
117
|
+
propertyName,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// if no attribute value was found, that means the attribute won't match
|
|
121
|
+
if (!attrs?.length) {
|
|
122
|
+
deleteNode(node)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// if the node attribute value won't match, then remove the node
|
|
127
|
+
if (comparator && !attrs.some(check)) {
|
|
128
|
+
deleteNode(node)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
removeDanglingEdges(state)
|
|
133
|
+
return state
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
|
|
137
|
+
const attributeSelectors: Record<string, ComparatorFn> = {
|
|
138
|
+
'=': (attr: string, value = '') => attr === value,
|
|
139
|
+
'^=': (attr: string, value = '') => attr.startsWith(value),
|
|
140
|
+
'$=': (attr: string, value = '') => attr.endsWith(value),
|
|
141
|
+
'~=': (attr: string, value = '') =>
|
|
142
|
+
new Set<string>(attr.match(/\w+/g)).has(value),
|
|
143
|
+
'*=': (attr: string, value = '') => attr.includes(value),
|
|
144
|
+
'|=': (attr: string, value = '') =>
|
|
145
|
+
attr === value || attr.startsWith(`${value}-`),
|
|
146
|
+
undefined: (attr: string) => !!attr,
|
|
147
|
+
}
|
|
148
|
+
export const attributeSelectorsMap = new Map<string, ComparatorFn>(
|
|
149
|
+
Object.entries(attributeSelectors),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse attributes selectors, e.g: `[name]`, `[name=value]`, etc
|
|
154
|
+
*/
|
|
155
|
+
export const attribute = async (
|
|
156
|
+
state: ParserState,
|
|
157
|
+
): Promise<ParserState> => {
|
|
158
|
+
await state.cancellable()
|
|
159
|
+
|
|
160
|
+
const curr = asAttributeNode(state.current)
|
|
161
|
+
const operatorFn = attributeSelectorsMap.get(String(curr.operator))
|
|
162
|
+
if (!operatorFn) {
|
|
163
|
+
if (state.loose) {
|
|
164
|
+
return state
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw error(`Unsupported attribute operator: ${curr.operator}`, {
|
|
168
|
+
found: state.current,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const value = curr.value || ''
|
|
173
|
+
const propertyName = curr.attribute
|
|
174
|
+
const insensitive = !!curr.insensitive
|
|
175
|
+
|
|
176
|
+
// Increment the commonCounter for specificity
|
|
177
|
+
state.specificity.commonCounter += 1
|
|
178
|
+
|
|
179
|
+
return filterAttributes(
|
|
180
|
+
state,
|
|
181
|
+
operatorFn,
|
|
182
|
+
value,
|
|
183
|
+
propertyName,
|
|
184
|
+
insensitive,
|
|
185
|
+
)
|
|
186
|
+
}
|