@sikkuumiii/defuse 0.0.0-stage → 0.5.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.
Files changed (236) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +561 -2
  3. package/dist/src/cli.d.ts +20 -0
  4. package/dist/src/cli.js +619 -0
  5. package/dist/src/cli.js.map +1 -0
  6. package/dist/src/core/analyze.d.ts +178 -0
  7. package/dist/src/core/analyze.js +834 -0
  8. package/dist/src/core/analyze.js.map +1 -0
  9. package/dist/src/core/coverage.d.ts +87 -0
  10. package/dist/src/core/coverage.js +181 -0
  11. package/dist/src/core/coverage.js.map +1 -0
  12. package/dist/src/core/finding.d.ts +196 -0
  13. package/dist/src/core/finding.js +178 -0
  14. package/dist/src/core/finding.js.map +1 -0
  15. package/dist/src/core/licence.d.ts +50 -0
  16. package/dist/src/core/licence.js +56 -0
  17. package/dist/src/core/licence.js.map +1 -0
  18. package/dist/src/core/paths.d.ts +21 -0
  19. package/dist/src/core/paths.js +58 -0
  20. package/dist/src/core/paths.js.map +1 -0
  21. package/dist/src/core/score.d.ts +137 -0
  22. package/dist/src/core/score.js +232 -0
  23. package/dist/src/core/score.js.map +1 -0
  24. package/dist/src/dast/targets.d.ts +30 -0
  25. package/dist/src/dast/targets.js +79 -0
  26. package/dist/src/dast/targets.js.map +1 -0
  27. package/dist/src/dast/zap.d.ts +42 -0
  28. package/dist/src/dast/zap.js +184 -0
  29. package/dist/src/dast/zap.js.map +1 -0
  30. package/dist/src/engine/scan.d.ts +80 -0
  31. package/dist/src/engine/scan.js +129 -0
  32. package/dist/src/engine/scan.js.map +1 -0
  33. package/dist/src/engine/shapes.d.ts +47 -0
  34. package/dist/src/engine/shapes.js +328 -0
  35. package/dist/src/engine/shapes.js.map +1 -0
  36. package/dist/src/engine/walk.d.ts +60 -0
  37. package/dist/src/engine/walk.js +156 -0
  38. package/dist/src/engine/walk.js.map +1 -0
  39. package/dist/src/parse/astDump.d.ts +31 -0
  40. package/dist/src/parse/astDump.js +66 -0
  41. package/dist/src/parse/astDump.js.map +1 -0
  42. package/dist/src/parse/languages.d.ts +70 -0
  43. package/dist/src/parse/languages.js +174 -0
  44. package/dist/src/parse/languages.js.map +1 -0
  45. package/dist/src/parse/location.d.ts +17 -0
  46. package/dist/src/parse/location.js +38 -0
  47. package/dist/src/parse/location.js.map +1 -0
  48. package/dist/src/parse/node-grammars.d.ts +12 -0
  49. package/dist/src/parse/node-grammars.js +35 -0
  50. package/dist/src/parse/node-grammars.js.map +1 -0
  51. package/dist/src/parse/parser.d.ts +126 -0
  52. package/dist/src/parse/parser.js +202 -0
  53. package/dist/src/parse/parser.js.map +1 -0
  54. package/dist/src/parse/tree-store.d.ts +139 -0
  55. package/dist/src/parse/tree-store.js +236 -0
  56. package/dist/src/parse/tree-store.js.map +1 -0
  57. package/dist/src/platform/dependency-rule.d.ts +27 -0
  58. package/dist/src/platform/dependency-rule.js +21 -0
  59. package/dist/src/platform/dependency-rule.js.map +1 -0
  60. package/dist/src/platform/deps.d.ts +86 -0
  61. package/dist/src/platform/deps.js +304 -0
  62. package/dist/src/platform/deps.js.map +1 -0
  63. package/dist/src/platform/php-facts.d.ts +49 -0
  64. package/dist/src/platform/php-facts.js +67 -0
  65. package/dist/src/platform/php-facts.js.map +1 -0
  66. package/dist/src/platform/runtime-version.d.ts +78 -0
  67. package/dist/src/platform/runtime-version.js +573 -0
  68. package/dist/src/platform/runtime-version.js.map +1 -0
  69. package/dist/src/report/colors.d.ts +50 -0
  70. package/dist/src/report/colors.js +97 -0
  71. package/dist/src/report/colors.js.map +1 -0
  72. package/dist/src/report/coverage-table.d.ts +90 -0
  73. package/dist/src/report/coverage-table.js +291 -0
  74. package/dist/src/report/coverage-table.js.map +1 -0
  75. package/dist/src/report/html-report.d.ts +66 -0
  76. package/dist/src/report/html-report.js +489 -0
  77. package/dist/src/report/html-report.js.map +1 -0
  78. package/dist/src/report/human.d.ts +44 -0
  79. package/dist/src/report/human.js +575 -0
  80. package/dist/src/report/human.js.map +1 -0
  81. package/dist/src/report/json.d.ts +24 -0
  82. package/dist/src/report/json.js +178 -0
  83. package/dist/src/report/json.js.map +1 -0
  84. package/dist/src/report/mark.d.ts +17 -0
  85. package/dist/src/report/mark.js +39 -0
  86. package/dist/src/report/mark.js.map +1 -0
  87. package/dist/src/report/pricing.d.ts +33 -0
  88. package/dist/src/report/pricing.js +343 -0
  89. package/dist/src/report/pricing.js.map +1 -0
  90. package/dist/src/report/sarif.d.ts +39 -0
  91. package/dist/src/report/sarif.js +219 -0
  92. package/dist/src/report/sarif.js.map +1 -0
  93. package/dist/src/report/site.d.ts +87 -0
  94. package/dist/src/report/site.js +742 -0
  95. package/dist/src/report/site.js.map +1 -0
  96. package/dist/src/rules/code-injection.d.ts +47 -0
  97. package/dist/src/rules/code-injection.js +326 -0
  98. package/dist/src/rules/code-injection.js.map +1 -0
  99. package/dist/src/rules/command-injection.d.ts +30 -0
  100. package/dist/src/rules/command-injection.js +286 -0
  101. package/dist/src/rules/command-injection.js.map +1 -0
  102. package/dist/src/rules/contract.d.ts +198 -0
  103. package/dist/src/rules/contract.js +39 -0
  104. package/dist/src/rules/contract.js.map +1 -0
  105. package/dist/src/rules/format-string.d.ts +46 -0
  106. package/dist/src/rules/format-string.js +146 -0
  107. package/dist/src/rules/format-string.js.map +1 -0
  108. package/dist/src/rules/hardcoded-secret.d.ts +38 -0
  109. package/dist/src/rules/hardcoded-secret.js +440 -0
  110. package/dist/src/rules/hardcoded-secret.js.map +1 -0
  111. package/dist/src/rules/lib/guards.d.ts +168 -0
  112. package/dist/src/rules/lib/guards.js +1179 -0
  113. package/dist/src/rules/lib/guards.js.map +1 -0
  114. package/dist/src/rules/lib/html-safety.d.ts +75 -0
  115. package/dist/src/rules/lib/html-safety.js +755 -0
  116. package/dist/src/rules/lib/html-safety.js.map +1 -0
  117. package/dist/src/rules/lib/local-build.d.ts +59 -0
  118. package/dist/src/rules/lib/local-build.js +320 -0
  119. package/dist/src/rules/lib/local-build.js.map +1 -0
  120. package/dist/src/rules/lib/strings.d.ts +208 -0
  121. package/dist/src/rules/lib/strings.js +1970 -0
  122. package/dist/src/rules/lib/strings.js.map +1 -0
  123. package/dist/src/rules/path-traversal.d.ts +33 -0
  124. package/dist/src/rules/path-traversal.js +96 -0
  125. package/dist/src/rules/path-traversal.js.map +1 -0
  126. package/dist/src/rules/registry.d.ts +19 -0
  127. package/dist/src/rules/registry.js +77 -0
  128. package/dist/src/rules/registry.js.map +1 -0
  129. package/dist/src/rules/sql-injection.d.ts +36 -0
  130. package/dist/src/rules/sql-injection.js +299 -0
  131. package/dist/src/rules/sql-injection.js.map +1 -0
  132. package/dist/src/rules/ssrf.d.ts +39 -0
  133. package/dist/src/rules/ssrf.js +111 -0
  134. package/dist/src/rules/ssrf.js.map +1 -0
  135. package/dist/src/rules/unbounded-copy.d.ts +37 -0
  136. package/dist/src/rules/unbounded-copy.js +147 -0
  137. package/dist/src/rules/unbounded-copy.js.map +1 -0
  138. package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
  139. package/dist/src/rules/unsafe-deserialization.js +276 -0
  140. package/dist/src/rules/unsafe-deserialization.js.map +1 -0
  141. package/dist/src/rules/weak-hash.d.ts +37 -0
  142. package/dist/src/rules/weak-hash.js +197 -0
  143. package/dist/src/rules/weak-hash.js.map +1 -0
  144. package/dist/src/rules/xss.d.ts +38 -0
  145. package/dist/src/rules/xss.js +527 -0
  146. package/dist/src/rules/xss.js.map +1 -0
  147. package/dist/src/taint/content-type.d.ts +19 -0
  148. package/dist/src/taint/content-type.js +20 -0
  149. package/dist/src/taint/content-type.js.map +1 -0
  150. package/dist/src/taint/dictionaries.d.ts +23 -0
  151. package/dist/src/taint/dictionaries.js +1778 -0
  152. package/dist/src/taint/dictionaries.js.map +1 -0
  153. package/dist/src/taint/fold.d.ts +59 -0
  154. package/dist/src/taint/fold.js +1177 -0
  155. package/dist/src/taint/fold.js.map +1 -0
  156. package/dist/src/taint/java-scope.d.ts +61 -0
  157. package/dist/src/taint/java-scope.js +212 -0
  158. package/dist/src/taint/java-scope.js.map +1 -0
  159. package/dist/src/taint/local-map.d.ts +79 -0
  160. package/dist/src/taint/local-map.js +603 -0
  161. package/dist/src/taint/local-map.js.map +1 -0
  162. package/dist/src/taint/project.d.ts +172 -0
  163. package/dist/src/taint/project.js +617 -0
  164. package/dist/src/taint/project.js.map +1 -0
  165. package/dist/src/taint/tracer.d.ts +265 -0
  166. package/dist/src/taint/tracer.js +3817 -0
  167. package/dist/src/taint/tracer.js.map +1 -0
  168. package/dist/src/taint/types.d.ts +484 -0
  169. package/dist/src/taint/types.js +47 -0
  170. package/dist/src/taint/types.js.map +1 -0
  171. package/dist/src/taint/visibility.d.ts +25 -0
  172. package/dist/src/taint/visibility.js +96 -0
  173. package/dist/src/taint/visibility.js.map +1 -0
  174. package/docs/examples/defuse.json +20 -0
  175. package/docs/examples/dvwa-urls.json +14 -0
  176. package/docs/examples/github-code-scanning.yml +54 -0
  177. package/docs/examples/tiny.js +4 -0
  178. package/package.json +73 -4
  179. package/src/cli.ts +747 -0
  180. package/src/core/analyze.ts +1072 -0
  181. package/src/core/coverage.ts +269 -0
  182. package/src/core/finding.ts +311 -0
  183. package/src/core/licence.ts +61 -0
  184. package/src/core/paths.ts +59 -0
  185. package/src/core/score.ts +353 -0
  186. package/src/dast/targets.ts +121 -0
  187. package/src/dast/zap.ts +246 -0
  188. package/src/engine/scan.ts +200 -0
  189. package/src/engine/shapes.ts +348 -0
  190. package/src/engine/walk.ts +196 -0
  191. package/src/parse/astDump.ts +85 -0
  192. package/src/parse/languages.ts +232 -0
  193. package/src/parse/location.ts +44 -0
  194. package/src/parse/node-grammars.ts +41 -0
  195. package/src/parse/parser.ts +262 -0
  196. package/src/parse/tree-store.ts +301 -0
  197. package/src/platform/dependency-rule.ts +33 -0
  198. package/src/platform/deps.ts +374 -0
  199. package/src/platform/php-facts.ts +85 -0
  200. package/src/platform/runtime-version.ts +710 -0
  201. package/src/report/colors.ts +107 -0
  202. package/src/report/coverage-table.ts +359 -0
  203. package/src/report/html-report.ts +601 -0
  204. package/src/report/human.ts +811 -0
  205. package/src/report/json.ts +189 -0
  206. package/src/report/mark.ts +47 -0
  207. package/src/report/pricing.ts +374 -0
  208. package/src/report/sarif.ts +235 -0
  209. package/src/report/site.ts +760 -0
  210. package/src/rules/code-injection.ts +346 -0
  211. package/src/rules/command-injection.ts +296 -0
  212. package/src/rules/contract.ts +220 -0
  213. package/src/rules/format-string.ts +158 -0
  214. package/src/rules/hardcoded-secret.ts +485 -0
  215. package/src/rules/lib/guards.ts +1235 -0
  216. package/src/rules/lib/html-safety.ts +793 -0
  217. package/src/rules/lib/local-build.ts +329 -0
  218. package/src/rules/lib/strings.ts +2034 -0
  219. package/src/rules/path-traversal.ts +101 -0
  220. package/src/rules/registry.ts +90 -0
  221. package/src/rules/sql-injection.ts +311 -0
  222. package/src/rules/ssrf.ts +118 -0
  223. package/src/rules/unbounded-copy.ts +162 -0
  224. package/src/rules/unsafe-deserialization.ts +291 -0
  225. package/src/rules/weak-hash.ts +226 -0
  226. package/src/rules/xss.ts +572 -0
  227. package/src/taint/content-type.ts +20 -0
  228. package/src/taint/dictionaries.ts +1855 -0
  229. package/src/taint/fold.ts +1206 -0
  230. package/src/taint/java-scope.ts +200 -0
  231. package/src/taint/local-map.ts +594 -0
  232. package/src/taint/project.ts +774 -0
  233. package/src/taint/tracer.ts +4298 -0
  234. package/src/taint/types.ts +511 -0
  235. package/src/taint/visibility.ts +100 -0
  236. package/tsconfig.json +61 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * THE TREE STORE (how a scan stops running out of memory)
3
+ * ========================================================
4
+ *
5
+ * THE PROBLEM, STATED HONESTLY.
6
+ *
7
+ * A tree-sitter syntax tree does not live in JavaScript memory. It lives in a
8
+ * WebAssembly heap that the garbage collector cannot see into, so a tree is
9
+ * never collected, however dead it looks - the only thing that frees one is an
10
+ * explicit call to tree.delete().
11
+ *
12
+ * Until this module existed, nothing in this project called delete(). Every file
13
+ * parsed stayed parsed until the process exited. Measured on Django:
14
+ *
15
+ * 248 files 0.9 MB source 173 MB peak
16
+ * 992 files 6.9 MB source 525 MB peak ~59x per marginal byte
17
+ *
18
+ * which puts the hard ceiling at roughly 44 MB of source on an 8 GB machine.
19
+ * Most serious repositories are bigger than that. The scan does not slow down
20
+ * at the ceiling; it is killed by the operating system, which is a worse
21
+ * failure than a slow answer because it produces no answer at all.
22
+ *
23
+ * THE EXPLANATION THAT WAS IN THE CODE, AND WHY IT WAS WRONG.
24
+ *
25
+ * A comment above the parse loop said the trees had to be held because
26
+ * cross-file tracing must be able to resolve into any file at any time, and
27
+ * that this was why `--no-cross-file` existed. Plausible, and false. Measured:
28
+ *
29
+ * cross-file ON 528.8 MB
30
+ * cross-file OFF 535.8 MB
31
+ *
32
+ * Turning the feature off freed nothing, because the trees were never freed in
33
+ * either mode. The stated cause was a rationalisation of an accident. It is
34
+ * recorded here because a wrong explanation is more expensive than no
35
+ * explanation: it stops anybody looking.
36
+ *
37
+ * WHAT THIS MODULE DOES.
38
+ *
39
+ * It owns every tree in the scan and holds as many as a stated budget allows,
40
+ * evicting the least recently used one when the budget is exceeded. A file
41
+ * whose tree was evicted is simply parsed again when something needs it. So:
42
+ *
43
+ * - a project that fits in the budget behaves exactly as before, parses each
44
+ * file exactly once, and runs at exactly the old speed;
45
+ * - a project that does not fit gets slower instead of getting killed.
46
+ *
47
+ * Time is a renewable resource and a dead process is not.
48
+ *
49
+ * PINNING - THE PART THAT WOULD BE A CRASH IF IT WERE WRONG.
50
+ *
51
+ * Evicting a tree frees the WebAssembly memory that every Node object from it
52
+ * points into. A Node whose tree has been deleted is not an error; it is a
53
+ * dangling pointer into a heap that now holds something else, and reading it
54
+ * yields silent nonsense. So a tree is pinned for as long as anything might
55
+ * still hold a node from it, and eviction skips pinned entries entirely, even
56
+ * when that means going over budget. Going over budget is visible in the stats
57
+ * and is the right answer: correctness first, then memory, then speed.
58
+ */
59
+
60
+ import type { Node, Tree } from 'web-tree-sitter';
61
+ import { parseSourceSync } from './parser.js';
62
+
63
+ /**
64
+ * How many bytes of WebAssembly heap one byte of source costs once parsed.
65
+ *
66
+ * MEASURED, not guessed: 6.9 MB of Django source produced a 525 MB peak against
67
+ * a ~120 MB floor (Node plus seven loaded grammars), which is about 59x. Sixty
68
+ * is used because the number varies by language and an underestimate is the
69
+ * dangerous direction - it would let the store believe it is inside a budget it
70
+ * has already blown.
71
+ *
72
+ * The estimate only decides WHEN to evict. Nothing is ever wrong because this
73
+ * number is off; the scan just holds slightly more or less than intended.
74
+ */
75
+ export const TREE_BYTES_PER_SOURCE_BYTE = 60;
76
+
77
+ /**
78
+ * Default budget: roughly a gigabyte of trees, which is about 17 MB of source
79
+ * held at once. Chosen so that every project this tool has been run against so
80
+ * far fits entirely and therefore parses each file exactly once - the redesign
81
+ * should cost nothing on the sizes people actually scan, and only start trading
82
+ * time for memory past the point where the old code died.
83
+ */
84
+ export const DEFAULT_TREE_BUDGET_BYTES = 1024 * 1024 * 1024;
85
+
86
+ export interface TreeStoreStats {
87
+ readonly budgetBytes: number;
88
+ /** Files whose tree is resident right now. */
89
+ readonly filesResident: number;
90
+ /** The most tree memory this scan ever estimated it was holding. */
91
+ readonly peakEstimatedBytes: number;
92
+ /** Parses performed in total, including the first one of each file. */
93
+ readonly parses: number;
94
+ /** Parses that happened only because an earlier tree had been evicted. */
95
+ readonly reparses: number;
96
+ readonly evictions: number;
97
+ /**
98
+ * True when the budget was exceeded and something had to be evicted - i.e.
99
+ * the project did not fit. Reported, because it is the difference between
100
+ * "this scan was slow" and "this scan was slow FOR A REASON".
101
+ */
102
+ readonly spilled: boolean;
103
+ /**
104
+ * Times the budget was blown because every resident tree was pinned. Not an
105
+ * error, but the one way this design can still use unbounded memory, so it is
106
+ * counted rather than assumed away.
107
+ */
108
+ readonly overBudgetWhileAllPinned: number;
109
+ }
110
+
111
+ interface Entry {
112
+ readonly path: string;
113
+ readonly source: string;
114
+ readonly grammar: string;
115
+ readonly estimatedBytes: number;
116
+ tree: Tree | null;
117
+ pins: number;
118
+ lastUsed: number;
119
+ }
120
+
121
+ export interface TreeStore {
122
+ /** Declare a file and hand over the tree the first pass already produced. */
123
+ admit(path: string, source: string, grammar: string, tree: Tree): void;
124
+ /** The tree for a known file, parsing it again if it was evicted. */
125
+ get(path: string): Tree;
126
+ /** Hold this file's tree until releaseAll(). Nested pins are counted. */
127
+ pin(path: string): void;
128
+ /** Drop every pin. Call only where no Node from any file is still held. */
129
+ releaseAll(): void;
130
+ /** Free everything. The store is unusable afterwards. */
131
+ disposeAll(): void;
132
+ stats(): TreeStoreStats;
133
+ }
134
+
135
+ /**
136
+ * Find the node at an exact byte range in a freshly parsed tree.
137
+ *
138
+ * This is the hinge of the whole design. The index records a function body as
139
+ * two byte offsets instead of a Node, because a Node keeps its tree alive and
140
+ * the point is to let trees go. When the tracer finally wants that body, the
141
+ * file is parsed again and the node is looked up here.
142
+ *
143
+ * Two things make it safe. The parse is deterministic - same bytes, same
144
+ * grammar, same tree - so the node is certainly there. And the result is
145
+ * CHECKED: the range and the node type must both match what was recorded, or
146
+ * this returns null and the caller declines to resolve. Tracing into a node we
147
+ * merely believe is the right one would manufacture exactly the kind of
148
+ * confident, wrong proof this project exists to avoid.
149
+ *
150
+ * The walk upward is needed because descendantForIndex returns the SMALLEST
151
+ * node covering a range, and a node can share its exact extent with its parent
152
+ * (a block that is its own statement, an expression that is its own body). The
153
+ * recorded type disambiguates them.
154
+ */
155
+ export function nodeAtRange(
156
+ root: Node,
157
+ startIndex: number,
158
+ endIndex: number,
159
+ type: string,
160
+ ): Node | null {
161
+ let node: Node | null = root.descendantForIndex(startIndex, endIndex);
162
+ let hops = 0;
163
+ while (node && hops < 64) {
164
+ if (node.startIndex === startIndex && node.endIndex === endIndex && node.type === type) {
165
+ return node;
166
+ }
167
+ node = node.parent;
168
+ hops++;
169
+ }
170
+ return null;
171
+ }
172
+
173
+ export function createTreeStore(budgetBytes = DEFAULT_TREE_BUDGET_BYTES): TreeStore {
174
+ const entries = new Map<string, Entry>();
175
+ let heldBytes = 0;
176
+ let peakEstimatedBytes = 0;
177
+ let clock = 0;
178
+ let parses = 0;
179
+ let reparses = 0;
180
+ let evictions = 0;
181
+ let spilled = false;
182
+ let overBudgetWhileAllPinned = 0;
183
+
184
+ const touch = (entry: Entry): void => {
185
+ entry.lastUsed = ++clock;
186
+ };
187
+
188
+ const drop = (entry: Entry): void => {
189
+ if (!entry.tree) return;
190
+ entry.tree.delete();
191
+ entry.tree = null;
192
+ heldBytes -= entry.estimatedBytes;
193
+ evictions++;
194
+ spilled = true;
195
+ };
196
+
197
+ /** Evict least-recently-used unpinned trees until we are inside the budget. */
198
+ const makeRoom = (): void => {
199
+ if (heldBytes <= budgetBytes) return;
200
+
201
+ const candidates = [...entries.values()]
202
+ .filter((e) => e.tree !== null && e.pins === 0)
203
+ .sort((a, b) => a.lastUsed - b.lastUsed);
204
+
205
+ for (const candidate of candidates) {
206
+ if (heldBytes <= budgetBytes) return;
207
+ drop(candidate);
208
+ }
209
+
210
+ // Everything still resident is pinned. We go over budget rather than free
211
+ // a tree somebody is holding a node from. Counted, never hidden.
212
+ if (heldBytes > budgetBytes) overBudgetWhileAllPinned++;
213
+ };
214
+
215
+ const note = (): void => {
216
+ if (heldBytes > peakEstimatedBytes) peakEstimatedBytes = heldBytes;
217
+ };
218
+
219
+ return {
220
+ admit(path, source, grammar, tree) {
221
+ const existing = entries.get(path);
222
+ if (existing) {
223
+ // Re-admitting the same path would leak the tree already held for it.
224
+ if (existing.tree && existing.tree !== tree) existing.tree.delete();
225
+ else if (existing.tree === null) heldBytes += existing.estimatedBytes;
226
+ existing.tree = tree;
227
+ touch(existing);
228
+ note();
229
+ makeRoom();
230
+ return;
231
+ }
232
+
233
+ const entry: Entry = {
234
+ path,
235
+ source,
236
+ grammar,
237
+ estimatedBytes: source.length * TREE_BYTES_PER_SOURCE_BYTE,
238
+ tree,
239
+ pins: 0,
240
+ lastUsed: ++clock,
241
+ };
242
+ entries.set(path, entry);
243
+ heldBytes += entry.estimatedBytes;
244
+ parses++;
245
+ note();
246
+ makeRoom();
247
+ },
248
+
249
+ get(path) {
250
+ const entry = entries.get(path);
251
+ if (!entry) {
252
+ throw new Error(
253
+ `No file registered at ${path}. Every file must be admitted during the ` +
254
+ 'first pass; asking for one that was not is a bug, not a missing file.',
255
+ );
256
+ }
257
+ if (entry.tree) {
258
+ touch(entry);
259
+ return entry.tree;
260
+ }
261
+ entry.tree = parseSourceSync(entry.source, entry.grammar);
262
+ heldBytes += entry.estimatedBytes;
263
+ parses++;
264
+ reparses++;
265
+ touch(entry);
266
+ note();
267
+ makeRoom();
268
+ return entry.tree;
269
+ },
270
+
271
+ pin(path) {
272
+ const entry = entries.get(path);
273
+ if (entry) entry.pins++;
274
+ },
275
+
276
+ releaseAll() {
277
+ for (const entry of entries.values()) entry.pins = 0;
278
+ makeRoom();
279
+ },
280
+
281
+ disposeAll() {
282
+ for (const entry of entries.values()) {
283
+ if (entry.tree) entry.tree.delete();
284
+ entry.tree = null;
285
+ }
286
+ heldBytes = 0;
287
+ entries.clear();
288
+ },
289
+
290
+ stats: () => ({
291
+ budgetBytes,
292
+ filesResident: [...entries.values()].filter((e) => e.tree !== null).length,
293
+ peakEstimatedBytes,
294
+ parses,
295
+ reparses,
296
+ evictions,
297
+ spilled,
298
+ overBudgetWhileAllPinned,
299
+ }),
300
+ };
301
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The dependency check's rule text and report shape, kept apart from deps.ts
3
+ * because the reports (which also run in the browser) need them and deps.ts
4
+ * reads the file system. Browser-safe: no imports.
5
+ */
6
+
7
+ export const DEPENDENCY_RULE = {
8
+ id: 'vulnerable-dependency',
9
+ name: 'Library version with a published advisory',
10
+ owasp: 'A03:2025 Software Supply Chain Failures',
11
+ summary: 'A library this project pins has a published security advisory for that exact version.',
12
+ fix: 'Upgrade the library to a fixed version and regenerate the lockfile.',
13
+ explanation:
14
+ 'Most of the code a project runs was written by someone else and arrives through its lockfile. ' +
15
+ 'When a pinned library version has a published advisory, the flaw is installed with it - whatever ' +
16
+ 'the project\'s own code does. The fix is usually an upgrade, not a code change.',
17
+ limitations:
18
+ 'Says the vulnerable version is installed, not that this project calls the vulnerable part - many ' +
19
+ 'advisories cover one function the project never uses. Reads package-lock.json, composer.lock, ' +
20
+ 'requirements.txt (== pins only) and go.mod; other lockfiles are listed as not read. Advisories come ' +
21
+ 'from osv.dev at scan time, so the same lockfile can gain findings tomorrow. Runs only with --deps, ' +
22
+ 'because it is the one check that sends anything off the machine: package names and versions, never code.',
23
+ } as const;
24
+
25
+ /** What the dependency check read and asked, for the report's "what was checked" section. */
26
+ export interface DependencyReport {
27
+ readonly source: 'api.osv.dev';
28
+ readonly checkedAt: string;
29
+ readonly read: ReadonlyArray<{ readonly file: string; readonly packages: number; readonly skipped: readonly string[] }>;
30
+ readonly notRead: readonly string[];
31
+ readonly checked: number;
32
+ readonly incomplete: readonly string[];
33
+ }