@nebzdev/bun-security-scanner 1.0.1 โ†’ 1.1.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/README.md CHANGED
@@ -4,15 +4,17 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Powered by OSV](https://img.shields.io/badge/powered%20by-OSV-4285F4.svg)](https://osv.dev)
6
6
  [![Powered by Snyk](https://img.shields.io/badge/powered%20by-Snyk-4C4A73.svg)](https://snyk.io)
7
+ ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/muneebs/bun-security-scanner?utm_source=oss&utm_medium=github&utm_campaign=muneebs%2Fbun-security-scanner&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)
7
8
 
8
9
  A [Bun security scanner](https://bun.com/docs/pm/security-scanner-api) that checks your dependencies against vulnerability databases before they get installed. Uses [Google's OSV database](https://osv.dev) by default โ€” no API keys required.
9
10
 
10
11
  - ๐Ÿ” **Automatic scanning**: runs transparently on every `bun install`
11
- - โšก **Fast**: 24-hour per-package lockfile cache means repeat installs skip the network entirely
12
+ - โšก **Fast**: per-package lockfile cache (24h by default, configurable) means repeat installs skip the network entirely
12
13
  - ๐Ÿ”€ **Two backends**: OSV (free, no setup) or Snyk (commercial, broader coverage)
13
14
  - ๐Ÿ”’ **Fail-open by default**: a downed API never blocks your install
14
- - ๐ŸŽฏ **CVSS fallback**: uses score-based severity when a label isn't available
15
- - ๐Ÿ› ๏ธ **Configurable**: tune behaviour via environment variables
15
+ - ๐ŸŽฏ **CVSS fallback**: falls back to score-based severity when a label isn't available
16
+ - ๐Ÿ™ˆ **Ignore file**: suppress false positives and accepted risks with `.bun-security-ignore`
17
+ - โš™๏ธ **Configurable**: tune behaviour via environment variables
16
18
 
17
19
  ---
18
20
 
@@ -31,15 +33,6 @@ scanner = "@nebzdev/bun-security-scanner"
31
33
 
32
34
  That's it. The scanner runs automatically on the next `bun install`.
33
35
 
34
- ### Local development
35
-
36
- Point `bunfig.toml` directly at the entry file using an absolute or relative path:
37
-
38
- ```toml
39
- [install.security]
40
- scanner = "../bun-osv-scanner/src/index.ts"
41
- ```
42
-
43
36
  ---
44
37
 
45
38
  ## ๐Ÿ”€ Backends
@@ -78,10 +71,10 @@ SNYK_ORG_ID=your-org-id
78
71
 
79
72
  When `bun install` runs, Bun calls the scanner with the full list of packages to be installed. The scanner:
80
73
 
81
- 1. **Filters** non-resolvable versions โ€” workspace, git, file, and path dependencies are skipped
82
- 2. **Checks the cache** โ€” packages seen within the last 24 hours skip the network entirely
83
- 3. **Queries the backend** for any uncached packages
84
- 4. **Returns advisories** to Bun, which surfaces them as warnings or fatal errors
74
+ 1. Filters non-resolvable versions โ€” workspace, git, file, and path dependencies are skipped
75
+ 2. Checks the cache โ€” packages seen within the cache TTL (24h by default) skip the network entirely
76
+ 3. Queries the backend for any uncached packages
77
+ 4. Returns advisories to Bun, which surfaces them as warnings or fatal errors
85
78
 
86
79
  ---
87
80
 
@@ -89,7 +82,7 @@ When `bun install` runs, Bun calls the scanner with the full list of packages to
89
82
 
90
83
  | Level | Trigger | Bun behaviour |
91
84
  |-------|---------|---------------|
92
- | `fatal` | CRITICAL or HIGH severity; or CVSS score โ‰ฅ 7.0 | Installation halts |
85
+ | `fatal` | CRITICAL or HIGH severity; or CVSS score >= 7.0 | Installation halts |
93
86
  | `warn` | MODERATE or LOW severity; or CVSS score < 7.0 | User is prompted; auto-cancelled in CI |
94
87
 
95
88
  ---
@@ -111,8 +104,10 @@ All options are set via environment variables โ€” in your shell, or in a `.env`
111
104
  | `OSV_FAIL_CLOSED` | `false` | Throw on network error instead of failing open |
112
105
  | `OSV_NO_CACHE` | `false` | Always query OSV fresh, bypassing the local cache |
113
106
  | `OSV_CACHE_FILE` | `.osv.lock` | Path to the cache file |
107
+ | `OSV_CACHE_TTL_MS` | `86400000` | Cache TTL in milliseconds (default: 24 hours) |
114
108
  | `OSV_TIMEOUT_MS` | `10000` | Per-request timeout in milliseconds |
115
109
  | `OSV_API_BASE` | `https://api.osv.dev/v1` | OSV API base URL |
110
+ | `OSV_NO_IGNORE` | `false` | Disable `.bun-security-ignore` processing |
116
111
 
117
112
  ### Snyk backend
118
113
 
@@ -123,6 +118,7 @@ All options are set via environment variables โ€” in your shell, or in a `.env`
123
118
  | `SNYK_FAIL_CLOSED` | `false` | Throw on network error instead of failing open |
124
119
  | `SNYK_NO_CACHE` | `false` | Always query Snyk fresh, bypassing the local cache |
125
120
  | `SNYK_CACHE_FILE` | `.snyk.lock` | Path to the cache file |
121
+ | `SNYK_CACHE_TTL_MS` | `86400000` | Cache TTL in milliseconds (default: 24 hours) |
126
122
  | `SNYK_TIMEOUT_MS` | `10000` | Per-request timeout in milliseconds |
127
123
  | `SNYK_RATE_LIMIT` | `160` | Max requests per minute (hard cap: 180) |
128
124
  | `SNYK_CONCURRENCY` | `10` | Max concurrent connections |
@@ -131,10 +127,10 @@ All options are set via environment variables โ€” in your shell, or in a `.env`
131
127
 
132
128
  ### Fail-open vs fail-closed
133
129
 
134
- By default the scanner **fails open**: if the backend is unreachable the scan is skipped and installation proceeds normally. Set `OSV_FAIL_CLOSED=true` or `SNYK_FAIL_CLOSED=true` to invert this.
130
+ By default the scanner fails open: if the backend is unreachable the scan is skipped and installation proceeds normally. Set `OSV_FAIL_CLOSED=true` or `SNYK_FAIL_CLOSED=true` to invert this.
135
131
 
136
132
  ```sh
137
- # .env โ€” strict mode
133
+ # .env -- strict mode
138
134
  OSV_FAIL_CLOSED=true
139
135
  ```
140
136
 
@@ -142,14 +138,22 @@ OSV_FAIL_CLOSED=true
142
138
 
143
139
  ## ๐Ÿ—„๏ธ Cache
144
140
 
145
- Results are cached per `package@version` in a lock file at the project root with a 24-hour TTL. Because a published package version is immutable, its vulnerability profile is stable within that window.
141
+ Results are cached per `package@version` in a lock file at the project root. Because a published package version is immutable, its vulnerability profile is stable within the cache window.
146
142
 
147
- | Backend | Lock file |
148
- |---------|-----------|
149
- | OSV | `.osv.lock` |
150
- | Snyk | `.snyk.lock` |
143
+ | Backend | Lock file | TTL env var |
144
+ |---------|-----------|-------------|
145
+ | OSV | `.osv.lock` | `OSV_CACHE_TTL_MS` |
146
+ | Snyk | `.snyk.lock` | `SNYK_CACHE_TTL_MS` |
151
147
 
152
- The files are designed to be **committed to git** โ€” similar to a lockfile, committing them means your team and CI share the cache from day one without waiting for a warm-up scan.
148
+ The default TTL is 24 hours. In CI environments where cold-start scan time is a concern, increase it:
149
+
150
+ ```sh
151
+ # .env.ci
152
+ OSV_CACHE_TTL_MS=604800000 # 7 days
153
+ SNYK_CACHE_TTL_MS=604800000 # 7 days
154
+ ```
155
+
156
+ The lock files are designed to be committed to git. Like a lockfile, committing them means your team and CI share the cache from day one without waiting for a warm-up scan.
153
157
 
154
158
  ```sh
155
159
  git add .osv.lock # or .snyk.lock
@@ -165,14 +169,74 @@ SNYK_NO_CACHE=true bun install
165
169
 
166
170
  ---
167
171
 
172
+ ## ๐Ÿ™ˆ Ignore file
173
+
174
+ Not every advisory is actionable. A vulnerability may affect a code path your project doesn't use, have no fix available yet, or be a false positive. The `.bun-security-ignore` file lets you acknowledge these cases without blocking installs permanently.
175
+
176
+ ### Format
177
+
178
+ ```toml
179
+ # .bun-security-ignore
180
+
181
+ [[ignore]]
182
+ package = "lodash"
183
+ advisories = ["GHSA-35jh-r3h4-6jhm"]
184
+ reason = "Only affects the cloneDeep path, which we do not use."
185
+ expires = "2026-12-31" # optional -- re-surfaces automatically after this date
186
+
187
+ [[ignore]]
188
+ package = "minimist"
189
+ advisories = ["*"] # wildcard -- suppress all advisories for this package
190
+ reason = "Transitive only, no direct usage, no fix available."
191
+ ```
192
+
193
+ ### Behaviour
194
+
195
+ | Advisory level | Session type | Effect |
196
+ |----------------|--------------|--------|
197
+ | `fatal` matched | Interactive (no `CI=true`, stdin is a TTY) | Downgraded to `warn` โ€” visible in output but no longer blocks the install |
198
+ | `fatal` matched | CI / non-interactive | Suppressed entirely โ€” logged to stderr but not returned |
199
+ | `warn` matched | Any | Suppressed entirely โ€” logged to stderr but not returned |
200
+
201
+ All suppressions are logged to stderr so they remain visible in CI output. Ignored advisories are never silently swallowed.
202
+
203
+ - `expires` -- entries re-activate at UTC midnight on the given date, so you're reminded when to reassess
204
+ - `advisories = ["*"]` -- wildcard suppresses all advisories for the package
205
+ - `reason` -- encouraged but not required; a notice is printed to stderr if omitted
206
+ - `OSV_NO_IGNORE=true` -- disables all ignore file processing for strict environments
207
+ - `BUN_SECURITY_IGNORE_FILE` -- override the default `.bun-security-ignore` path
208
+
209
+ ### Environment variables
210
+
211
+ | Variable | Default | Description |
212
+ |----------|---------|-------------|
213
+ | `BUN_SECURITY_IGNORE_FILE` | `.bun-security-ignore` | Path to the ignore file |
214
+ | `OSV_NO_IGNORE` | `false` | Disable all ignore file processing |
215
+
216
+ ### Committing the file
217
+
218
+ The ignore file should be committed alongside your lockfile. It documents deliberate risk-acceptance decisions for your whole team and CI.
219
+
220
+ ---
221
+
168
222
  ## ๐Ÿ› ๏ธ Development
169
223
 
170
224
  ### Setup
171
225
 
172
226
  ```sh
173
- git clone https://github.com/muneebs/bun-osv-scanner.git
174
- cd bun-osv-scanner
227
+ git clone https://github.com/muneebs/bun-security-scanner.git
228
+ cd bun-security-scanner
175
229
  bun install
230
+ bunx lefthook install
231
+ ```
232
+
233
+ ### Local development
234
+
235
+ Point `bunfig.toml` directly at the entry file using an absolute or relative path:
236
+
237
+ ```toml
238
+ [install.security]
239
+ scanner = "../bun-security-scanner/src/index.ts"
176
240
  ```
177
241
 
178
242
  ### Commands
@@ -189,17 +253,20 @@ bun run check:write # Lint + format, auto-fix what it can
189
253
  ### Project structure
190
254
 
191
255
  ```
192
- bun-osv-scanner/
256
+ bun-security-scanner/
193
257
  โ”œโ”€โ”€ src/
194
258
  โ”‚ โ”œโ”€โ”€ __tests__/ # Test suite (bun:test)
195
259
  โ”‚ โ”œโ”€โ”€ snyk/ # Snyk backend
196
- โ”‚ โ”œโ”€โ”€ cache.ts # 24h lockfile cache
260
+ โ”‚ โ”œโ”€โ”€ cache.ts # Lockfile cache (configurable TTL)
197
261
  โ”‚ โ”œโ”€โ”€ client.ts # OSV API client
198
262
  โ”‚ โ”œโ”€โ”€ config.ts # OSV constants and env vars
199
263
  โ”‚ โ”œโ”€โ”€ display.ts # TTY progress spinner
200
- โ”‚ โ”œโ”€โ”€ index.ts # Entry point โ€” dispatches to OSV or Snyk
264
+ โ”‚ โ”œโ”€โ”€ ignore.ts # .bun-security-ignore loader and matcher
265
+ โ”‚ โ”œโ”€โ”€ index.ts # Entry point -- dispatches to OSV or Snyk
201
266
  โ”‚ โ”œโ”€โ”€ osv.ts # OSV scanner implementation
267
+ โ”‚ โ”œโ”€โ”€ scanner.ts # Shared scanner factory (cache + ignore orchestration)
202
268
  โ”‚ โ””โ”€โ”€ severity.ts # OSV level classification
269
+ โ”œโ”€โ”€ dist/ # Compiled output (published to npm)
203
270
  โ”œโ”€โ”€ bunfig.toml
204
271
  โ””โ”€โ”€ package.json
205
272
  ```
@@ -218,7 +285,7 @@ bun-osv-scanner/
218
285
  ## โš ๏ธ Limitations
219
286
 
220
287
  - Only scans npm packages with concrete semver versions. `workspace:`, `file:`, `git:`, and range-only specifiers are skipped.
221
- - OSV aggregates GitHub Advisory, NVD, and other feeds โ€” coverage may lag slightly behind a vulnerability's public disclosure.
288
+ - OSV aggregates GitHub Advisory, NVD, and other feeds, so coverage may lag slightly behind a vulnerability's public disclosure.
222
289
  - The OSV batch API has a hard limit of 1,000 queries per request. Larger projects are split across multiple requests automatically.
223
290
  - Snyk's per-package endpoint is rate-limited to 180 req/min. At that rate, a project with 2,000+ packages will take several minutes on the first scan.
224
291
 
@@ -233,6 +300,6 @@ MIT ยฉ [Muneeb Samuels](https://github.com/muneebs)
233
300
  ## ๐Ÿ”— Links
234
301
 
235
302
  - [๐Ÿ“ฆ npm](https://www.npmjs.com/package/@nebzdev/bun-security-scanner)
236
- - [๐Ÿ› Issue tracker](https://github.com/muneebs/bun-osv-scanner/issues)
303
+ - [๐Ÿ› Issue tracker](https://github.com/muneebs/bun-security-scanner/issues)
237
304
  - [๐Ÿ” OSV database](https://osv.dev)
238
- - [๐Ÿ“– Bun security scanner docs](https://bun.com/docs/pm/security-scanner-api)
305
+ - [๐Ÿ“– Bun security scanner docs](https://bun.com/docs/pm/security-scanner-api)
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // @bun
2
+ var Z=Bun.env.OSV_API_BASE??"https://api.osv.dev/v1",C=1000,K=Number(Bun.env.OSV_TIMEOUT_MS)||1e4,F=["ADVISORY","WEB","ARTICLE"],G=Bun.env.OSV_CACHE_FILE??".osv.lock",A=Number(Bun.env.OSV_CACHE_TTL_MS),L=Number.isFinite(A)&&A>=0?A:86400000,N=Bun.env.OSV_FAIL_CLOSED==="true",Y=Bun.env.OSV_NO_CACHE==="true";function O(f,$){let x=new AbortController,j=setTimeout(()=>x.abort(),K);return fetch(f,{...$,signal:x.signal}).finally(()=>clearTimeout(j))}function P(f){return/^v?\d+\.\d+/.test(f)}async function t(f){let $=[];for(let x=0;x<f.length;x+=C){let j=f.slice(x,x+C),z=await O(`${Z}/querybatch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({queries:j.map((X)=>({version:X.version,package:{name:X.name,ecosystem:"npm"}}))})});if(!z.ok){let X=await z.text().catch(()=>"");throw Error(`OSV API ${z.status}: ${X||z.statusText}`)}let{results:B}=await z.json();$.push(...B)}return $}async function E(f){let $=await O(`${Z}/vulns/${f}`);if(!$.ok)return null;return $.json()}import{rename as zf}from"fs/promises";var jf=86400000;function Jf(f){if(typeof f!=="object"||f===null||Array.isArray(f))return!1;return Object.values(f).every(($)=>typeof $==="object"&&$!==null&&Array.isArray($.advisories)&&typeof $.cachedAt==="number")}async function w(f){try{let $=JSON.parse(await Bun.file(f).text());return Jf($)?$:{}}catch{return{}}}async function v(f,$){try{let x=`${$}.tmp`;await Bun.write(x,JSON.stringify(f,null,2)),await zf(x,$)}catch{}}function b(f,$=jf){return Date.now()-f.cachedAt<$}function I(f){if(!process.stderr.isTTY)return{update:(B)=>{},stop:()=>{}};let $=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],x=0,j=f;process.stderr.write(`${$[0]} ${j}`);let z=setInterval(()=>process.stderr.write(`\r${$[++x%$.length]} ${j}`),80);return{update(B){j=B},stop(){clearInterval(z),process.stderr.write("\r\x1B[2K")}}}var R=Bun.env.BUN_SECURITY_IGNORE_FILE??".bun-security-ignore",Xf=Bun.env.OSV_NO_IGNORE==="true";function mf(f){let x=Bun.TOML.parse(f).ignore;if(!Array.isArray(x))return[];let j=[];for(let z of x){if(typeof z!=="object"||z===null)continue;let B=z;if(typeof B.package!=="string"||!B.package)continue;let X=Array.isArray(B.advisories)?B.advisories.filter((D)=>typeof D==="string"):[],J={package:B.package,advisories:X};if(typeof B.reason==="string")J.reason=B.reason;if(typeof B.expires==="string")J.expires=B.expires;else if(B.expires instanceof Date)J.expires=B.expires.toISOString().slice(0,10);j.push(J)}return j}async function l(){if(Xf)return{entries:[]};let f;try{f=await Bun.file(R).text()}catch{return{entries:[]}}let $;try{$=mf(f)}catch(x){return process.stderr.write(`[@nebzdev/bun-security-scanner] Warning: failed to parse "${R}" \u2014 ${x instanceof Error?x.message:x}. All ignore entries will be skipped.
3
+ `),{entries:[]}}for(let x of $)if(!x.reason)process.stderr.write(`[@nebzdev/bun-security-scanner] Warning: ignore entry for "${x.package}" has no reason \u2014 consider documenting why.
4
+ `);return{entries:$}}function Df(f){if(!f.expires)return!0;let $=new Date(`${f.expires}T00:00:00Z`);return Date.now()<$.getTime()}function Hf(f){return f?.split("/").pop()?.toUpperCase()??""}function T(f,$){let x=Hf(f.url);for(let j of $.entries){if(j.package!==f.package)continue;if(!Df(j))continue;if(!(j.advisories.includes("*")||j.advisories.map((J)=>J.toUpperCase()).includes(x)))continue;let X=j.reason??"(no reason provided)";if(f.level==="fatal")return{action:"downgrade",reason:X};return{action:"drop",reason:X}}return{action:"keep"}}function S(f){return{version:"1",async scan({packages:$}){f.validateConfig?.();let x=$.filter((m)=>m.name&&P(m.version));if(x.length===0)return[];let[j,z]=await Promise.all([f.noCache?Promise.resolve({}):w(f.cacheFile),l()]),B=[],X=[];for(let m of x){let H=j[`${m.name}@${m.version}`];if(H&&b(H,f.ttl))B.push(...H.advisories);else X.push(m)}if(X.length===0)return V(B,z);let J=x.length-X.length,D=I(J>0?`Scanning ${X.length} packages via ${f.name} (${J} cached)...`:`Scanning ${x.length} packages via ${f.name}...`);try{let m=await f.fetchAdvisories(X,(H)=>D.update(H));D.stop();for(let[H,xf]of m)j[H]={advisories:xf,cachedAt:Date.now()};if(!f.noCache)v(j,f.cacheFile);return V([...B,...[...m.values()].flat()],z)}catch(m){if(D.stop(),f.failClosed)throw Error(`${f.name} scan failed: ${m instanceof Error?m.message:m}`);return process.stderr.write(`
5
+ ${f.name} scan failed (${m instanceof Error?m.message:m}), skipping.
6
+ `),V(B,z)}}}}function V(f,$){if($.entries.length===0)return f;let x=process.env.CI!=="true"&&(process.stdin?.isTTY??!1),j=[];for(let z of f){let B=T(z,$);if(B.action==="keep")j.push(z);else if(B.action==="downgrade"&&x)process.stderr.write(`[@nebzdev/bun-security-scanner] Downgrading ${z.package} fatal advisory to warn (${z.url}) \u2014 ${B.reason}
7
+ `),j.push({...z,level:"warn"});else process.stderr.write(`[@nebzdev/bun-security-scanner] Suppressing ${z.package} advisory (${z.url}) \u2014 ${B.reason}
8
+ `)}return j}function _(f){let $=f.database_specific?.severity?.toUpperCase();if($==="CRITICAL"||$==="HIGH")return"fatal";if($==="MODERATE"||$==="LOW")return"warn";let x=f.database_specific?.cvss?.score;if(typeof x==="number")return x>=7?"fatal":"warn";return"warn"}function p(f){for(let $ of F){let x=f.references?.find((j)=>j.type===$);if(x)return x.url}return`https://osv.dev/vulnerability/${f.id}`}var U={name:"OSV",cacheFile:G,ttl:L,noCache:Y,failClosed:N,async fetchAdvisories(f,$){let x=await t(f),j=[];for(let B=0;B<f.length;B++)for(let{id:X}of x[B]?.vulns??[])j.push({pkg:f[B],vulnId:X});let z=new Map;for(let B of f)z.set(`${B.name}@${B.version}`,[]);if(j.length>0){let B=[...new Set(j.map((J)=>J.vulnId))],X=new Map;$(`Fetching details for ${B.length} ${B.length===1?"vulnerability":"vulnerabilities"}...`),await Promise.all(B.map(async(J)=>{let D=await E(J);if(D)X.set(J,D)}));for(let{pkg:J,vulnId:D}of j){let m=X.get(D);if(m)z.get(`${J.name}@${J.version}`)?.push({level:_(m),package:J.name,url:p(m),description:m.summary??m.id})}}return z}};var vf=S(U);var n=Bun.env.SNYK_API_BASE??"https://api.snyk.io/rest",g=Bun.env.SNYK_API_VERSION??"2024-04-29",u=Bun.env.SNYK_TOKEN,M=Bun.env.SNYK_ORG_ID,c=Number(Bun.env.SNYK_TIMEOUT_MS)||1e4,k=Bun.env.SNYK_FAIL_CLOSED==="true",y=Bun.env.SNYK_NO_CACHE==="true",Q=Number(Bun.env.SNYK_CONCURRENCY)||10,h=Math.min(Number(Bun.env.SNYK_RATE_LIMIT)||160,180),o=Bun.env.SNYK_CACHE_FILE??".snyk.lock",q=Number(Bun.env.SNYK_CACHE_TTL_MS),d=Number.isFinite(q)&&q>=0?q:86400000;class i{limit;timestamps=[];windowMs=60000;constructor(f){this.limit=f}async acquire(){let f=Date.now();while(this.timestamps.length>0&&f-this.timestamps[0]>=this.windowMs)this.timestamps.shift();if(this.timestamps.length<this.limit){this.timestamps.push(Date.now());return}let $=this.windowMs-(Date.now()-this.timestamps[0])+10;return await Bun.sleep($),this.acquire()}}var Sf=new i(h);function Uf(f,$){let x=new AbortController,j=setTimeout(()=>x.abort(),c);return fetch(f,{...$,signal:x.signal}).finally(()=>clearTimeout(j))}function s(){if(!u)throw Error("SNYK_TOKEN is required for the Snyk scanner");if(!M)throw Error("SNYK_ORG_ID is required for the Snyk scanner")}function Af(f,$){if(f.startsWith("@")){let x=f.indexOf("/",1),j=f.slice(1,x),z=f.slice(x+1);return`pkg:npm/${j}/${z}@${$}`}return`pkg:npm/${f}@${$}`}async function r(f,$,x=3){await Sf.acquire();let j=encodeURIComponent(Af(f,$)),z=`${n}/orgs/${M}/packages/${j}/issues?version=${g}&limit=1000`,B=await Uf(z,{headers:{Authorization:`token ${u}`,"Content-Type":"application/vnd.api+json"}});if(B.status===429&&x>0){let J=B.headers.get("Retry-After"),D=J?Number(J)*1000:60000;return await Bun.sleep(D),r(f,$,x-1)}if(B.status===404)return[];if(!B.ok){let J=await B.text().catch(()=>"");throw Error(`Snyk API ${B.status}: ${J||B.statusText}`)}let{data:X}=await B.json();return X??[]}async function a(f,$){let x=new Map,j=0;for(let z=0;z<f.length;z+=Q)await Promise.all(f.slice(z,z+Q).map(async(B)=>{let X=await r(B.name,B.version);x.set(`${B.name}@${B.version}`,X),$?.(++j,f.length)}));return x}function e(f){let $=f.attributes.effective_severity_level;return $==="critical"||$==="high"?"fatal":"warn"}function ff(f){return`https://security.snyk.io/vuln/${f.id}`}var W={name:"Snyk",cacheFile:o,ttl:d,noCache:y,failClosed:k,validateConfig:s,async fetchAdvisories(f,$){let x=await a(f,(z,B)=>{$(`Scanning packages via Snyk (${z}/${B})...`)}),j=new Map;for(let z of f){let B=`${z.name}@${z.version}`,X=x.get(B)??[];j.set(B,X.map((J)=>({level:e(J),package:z.name,url:ff(J),description:J.attributes.title})))}return j}};var cf=S(W);var Zf={osv:U,snyk:W},$f=(Bun.env.SCANNER_BACKEND??"osv").toLowerCase(),Bf=Zf[$f];if(!Bf)process.stderr.write(`[@nebzdev/bun-security-scanner] Unknown SCANNER_BACKEND "${$f}", falling back to osv.
9
+ `);var df=S(Bf??U);export{df as scanner};
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "@nebzdev/bun-security-scanner",
3
3
  "description": "Bun security scanner powered by Google's OSV vulnerability database",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "author": "Muneeb Samuels",
6
6
  "license": "MIT",
7
7
  "exports": {
8
8
  "./package.json": "./package.json",
9
- ".": "./src/index.ts"
9
+ ".": "./dist/index.js"
10
10
  },
11
11
  "files": [
12
- "src",
13
- "!src/__tests__",
12
+ "dist",
14
13
  "README.md"
15
14
  ],
16
15
  "keywords": [
@@ -22,16 +21,19 @@
22
21
  "vulnerability"
23
22
  ],
24
23
  "scripts": {
25
- "publish:npm": "npm publish --access public",
24
+ "build": "bun run build.ts",
25
+ "publish:npm": "bun run build && npm publish --access public",
26
26
  "lint": "biome lint ./src",
27
27
  "format": "biome format ./src",
28
28
  "format:write": "biome format --write ./src",
29
29
  "check": "biome check ./src",
30
- "check:write": "biome check --write ./src"
30
+ "check:write": "biome check --write ./src",
31
+ "type-check": "tsc --noEmit"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@biomejs/biome": "2.4.10",
34
35
  "@types/bun": "latest",
36
+ "lefthook": "^2.1.4",
35
37
  "typescript": "^5.0.0"
36
38
  }
37
39
  }
package/src/cache.ts DELETED
@@ -1,48 +0,0 @@
1
- import { rename } from 'node:fs/promises';
2
-
3
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
4
-
5
- export interface CacheEntry {
6
- advisories: Bun.Security.Advisory[];
7
- cachedAt: number;
8
- }
9
-
10
- type Cache = Record<string, CacheEntry>;
11
-
12
- function isValidCache(data: unknown): data is Cache {
13
- if (typeof data !== 'object' || data === null || Array.isArray(data))
14
- return false;
15
- return Object.values(data).every(
16
- (entry) =>
17
- typeof entry === 'object' &&
18
- entry !== null &&
19
- Array.isArray((entry as CacheEntry).advisories) &&
20
- typeof (entry as CacheEntry).cachedAt === 'number'
21
- );
22
- }
23
-
24
- export async function readCache(cacheFile: string): Promise<Cache> {
25
- try {
26
- const data: unknown = JSON.parse(await Bun.file(cacheFile).text());
27
- return isValidCache(data) ? data : {};
28
- } catch {
29
- return {};
30
- }
31
- }
32
-
33
- export async function writeCache(
34
- cache: Cache,
35
- cacheFile: string
36
- ): Promise<void> {
37
- try {
38
- // Write to a temp file first, then rename โ€” prevents partial-write corruption
39
- // if the process is killed or two installs run concurrently.
40
- const tmp = `${cacheFile}.tmp`;
41
- await Bun.write(tmp, JSON.stringify(cache, null, 2));
42
- await rename(tmp, cacheFile);
43
- } catch {}
44
- }
45
-
46
- export function isFresh(entry: CacheEntry): boolean {
47
- return Date.now() - entry.cachedAt < CACHE_TTL_MS;
48
- }
package/src/client.ts DELETED
@@ -1,72 +0,0 @@
1
- import { FETCH_TIMEOUT_MS, OSV_API_BASE, OSV_BATCH_SIZE } from './config';
2
-
3
- export interface OsvBatchResponse {
4
- results: Array<{
5
- vulns?: Array<{ id: string; modified: string }>;
6
- next_page_token?: string;
7
- }>;
8
- }
9
-
10
- export interface OsvVulnerability {
11
- id: string;
12
- summary?: string;
13
- references?: Array<{ type: string; url: string }>;
14
- severity?: Array<{ type: string; score: string }>;
15
- database_specific?: {
16
- severity?: string;
17
- cvss?: { score?: number };
18
- [key: string]: unknown;
19
- };
20
- }
21
-
22
- function fetchWithTimeout(
23
- url: string,
24
- options?: RequestInit
25
- ): Promise<Response> {
26
- const controller = new AbortController();
27
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
28
- return fetch(url, { ...options, signal: controller.signal }).finally(() =>
29
- clearTimeout(timer)
30
- );
31
- }
32
-
33
- // workspace:, file:, git:, and range specifiers cause a 400 for the whole batch.
34
- export function isResolvable(version: string): boolean {
35
- return /^v?\d+\.\d+/.test(version);
36
- }
37
-
38
- export async function batchQuery(
39
- packages: Bun.Security.Package[]
40
- ): Promise<OsvBatchResponse['results']> {
41
- const results: OsvBatchResponse['results'] = [];
42
-
43
- for (let i = 0; i < packages.length; i += OSV_BATCH_SIZE) {
44
- const chunk = packages.slice(i, i + OSV_BATCH_SIZE);
45
- const res = await fetchWithTimeout(`${OSV_API_BASE}/querybatch`, {
46
- method: 'POST',
47
- headers: { 'Content-Type': 'application/json' },
48
- body: JSON.stringify({
49
- queries: chunk.map((p) => ({
50
- version: p.version,
51
- package: { name: p.name, ecosystem: 'npm' },
52
- })),
53
- }),
54
- });
55
-
56
- if (!res.ok) {
57
- const body = await res.text().catch(() => '');
58
- throw new Error(`OSV API ${res.status}: ${body || res.statusText}`);
59
- }
60
-
61
- const { results: chunkResults } = (await res.json()) as OsvBatchResponse;
62
- results.push(...chunkResults);
63
- }
64
-
65
- return results;
66
- }
67
-
68
- export async function fetchVuln(id: string): Promise<OsvVulnerability | null> {
69
- const res = await fetchWithTimeout(`${OSV_API_BASE}/vulns/${id}`);
70
- if (!res.ok) return null;
71
- return res.json() as Promise<OsvVulnerability>;
72
- }
package/src/config.ts DELETED
@@ -1,11 +0,0 @@
1
- export const OSV_API_BASE = Bun.env.OSV_API_BASE ?? 'https://api.osv.dev/v1';
2
- // Hard limit enforced by the OSV API โ€” exceeding it returns 400 "Too many queries".
3
- export const OSV_BATCH_SIZE = 1000;
4
- export const FETCH_TIMEOUT_MS = Number(Bun.env.OSV_TIMEOUT_MS) || 10_000;
5
- export const PREFERRED_REF_TYPES = ['ADVISORY', 'WEB', 'ARTICLE'] as const;
6
- export const CACHE_FILE = Bun.env.OSV_CACHE_FILE ?? '.osv.lock';
7
- export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
8
-
9
- // When true, network failures throw and cancel installation rather than failing open.
10
- export const FAIL_CLOSED = Bun.env.OSV_FAIL_CLOSED === 'true';
11
- export const NO_CACHE = Bun.env.OSV_NO_CACHE === 'true';
package/src/display.ts DELETED
@@ -1,24 +0,0 @@
1
- export function startSpinner(message: string) {
2
- if (!process.stderr.isTTY)
3
- return { update: (_: string) => {}, stop: () => {} };
4
-
5
- const frames = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ‡', 'โ '];
6
- let i = 0;
7
- let current = message;
8
-
9
- process.stderr.write(`${frames[0]} ${current}`);
10
- const interval = setInterval(
11
- () => process.stderr.write(`\r${frames[++i % frames.length]} ${current}`),
12
- 80
13
- );
14
-
15
- return {
16
- update(msg: string) {
17
- current = msg;
18
- },
19
- stop() {
20
- clearInterval(interval);
21
- process.stderr.write('\r\x1b[2K');
22
- },
23
- };
24
- }
package/src/index.ts DELETED
@@ -1,21 +0,0 @@
1
- import { backend as osvBackend } from './osv';
2
- import { type Backend, createScanner } from './scanner';
3
- import { backend as snykBackend } from './snyk/index';
4
-
5
- const registry: Record<string, Backend> = {
6
- osv: osvBackend,
7
- snyk: snykBackend,
8
- };
9
-
10
- const backendName = (Bun.env.SCANNER_BACKEND ?? 'osv').toLowerCase();
11
- const selected = registry[backendName];
12
-
13
- if (!selected) {
14
- process.stderr.write(
15
- `[@nebzdev/bun-security-scanner] Unknown SCANNER_BACKEND "${backendName}", falling back to osv.\n`
16
- );
17
- }
18
-
19
- export const scanner: Bun.Security.Scanner = createScanner(
20
- selected ?? osvBackend
21
- );
package/src/osv.ts DELETED
@@ -1,61 +0,0 @@
1
- import type { OsvVulnerability } from './client';
2
- import { batchQuery, fetchVuln } from './client';
3
- import { CACHE_FILE, FAIL_CLOSED, NO_CACHE } from './config';
4
- import { type Backend, createScanner } from './scanner';
5
- import { advisoryUrl, severityLevel } from './severity';
6
-
7
- const backend: Backend = {
8
- name: 'OSV',
9
- cacheFile: CACHE_FILE,
10
- noCache: NO_CACHE,
11
- failClosed: FAIL_CLOSED,
12
-
13
- async fetchAdvisories(packages, onStatus) {
14
- const batchResults = await batchQuery(packages);
15
-
16
- const affected: Array<{ pkg: Bun.Security.Package; vulnId: string }> = [];
17
- for (let i = 0; i < packages.length; i++) {
18
- for (const { id } of batchResults[i]?.vulns ?? []) {
19
- affected.push({ pkg: packages[i], vulnId: id });
20
- }
21
- }
22
-
23
- const result = new Map<string, Bun.Security.Advisory[]>();
24
- for (const pkg of packages) {
25
- result.set(`${pkg.name}@${pkg.version}`, []);
26
- }
27
-
28
- if (affected.length > 0) {
29
- const uniqueIds = [...new Set(affected.map((a) => a.vulnId))];
30
- const vulnById = new Map<string, OsvVulnerability>();
31
-
32
- onStatus(
33
- `Fetching details for ${uniqueIds.length} ${uniqueIds.length === 1 ? 'vulnerability' : 'vulnerabilities'}...`
34
- );
35
-
36
- await Promise.all(
37
- uniqueIds.map(async (id) => {
38
- const vuln = await fetchVuln(id);
39
- if (vuln) vulnById.set(id, vuln);
40
- })
41
- );
42
-
43
- for (const { pkg, vulnId } of affected) {
44
- const vuln = vulnById.get(vulnId);
45
- if (vuln) {
46
- result.get(`${pkg.name}@${pkg.version}`)?.push({
47
- level: severityLevel(vuln),
48
- package: pkg.name,
49
- url: advisoryUrl(vuln),
50
- description: vuln.summary ?? vuln.id,
51
- });
52
- }
53
- }
54
- }
55
-
56
- return result;
57
- },
58
- };
59
-
60
- export { backend };
61
- export const scanner = createScanner(backend);
package/src/scanner.ts DELETED
@@ -1,81 +0,0 @@
1
- import { isFresh, readCache, writeCache } from './cache';
2
- import { isResolvable } from './client';
3
- import { startSpinner } from './display';
4
-
5
- export interface Backend {
6
- readonly name: string;
7
- readonly cacheFile: string;
8
- readonly noCache: boolean;
9
- readonly failClosed: boolean;
10
- validateConfig?(): void;
11
- fetchAdvisories(
12
- packages: Bun.Security.Package[],
13
- onStatus: (message: string) => void
14
- ): Promise<Map<string, Bun.Security.Advisory[]>>;
15
- }
16
-
17
- export function createScanner(backend: Backend): Bun.Security.Scanner {
18
- return {
19
- version: '1',
20
-
21
- async scan({ packages }) {
22
- backend.validateConfig?.();
23
-
24
- const queryable = packages.filter(
25
- (p) => p.name && isResolvable(p.version)
26
- );
27
- if (queryable.length === 0) return [];
28
-
29
- const cache = backend.noCache ? {} : await readCache(backend.cacheFile);
30
-
31
- const cachedAdvisories: Bun.Security.Advisory[] = [];
32
- const toQuery: Bun.Security.Package[] = [];
33
-
34
- for (const pkg of queryable) {
35
- const entry = cache[`${pkg.name}@${pkg.version}`];
36
- if (entry && isFresh(entry)) {
37
- cachedAdvisories.push(...entry.advisories);
38
- } else {
39
- toQuery.push(pkg);
40
- }
41
- }
42
-
43
- if (toQuery.length === 0) return cachedAdvisories;
44
-
45
- const hitCount = queryable.length - toQuery.length;
46
- const spinner = startSpinner(
47
- hitCount > 0
48
- ? `Scanning ${toQuery.length} packages via ${backend.name} (${hitCount} cached)...`
49
- : `Scanning ${queryable.length} packages via ${backend.name}...`
50
- );
51
-
52
- try {
53
- const advisoryMap = await backend.fetchAdvisories(toQuery, (msg) =>
54
- spinner.update(msg)
55
- );
56
-
57
- spinner.stop();
58
-
59
- for (const [key, advisories] of advisoryMap) {
60
- cache[key] = { advisories, cachedAt: Date.now() };
61
- }
62
- if (!backend.noCache) void writeCache(cache, backend.cacheFile);
63
-
64
- return [...cachedAdvisories, ...[...advisoryMap.values()].flat()];
65
- } catch (err) {
66
- spinner.stop();
67
-
68
- if (backend.failClosed) {
69
- throw new Error(
70
- `${backend.name} scan failed: ${err instanceof Error ? err.message : err}`
71
- );
72
- }
73
-
74
- process.stderr.write(
75
- `\n${backend.name} scan failed (${err instanceof Error ? err.message : err}), skipping.\n`
76
- );
77
- return cachedAdvisories;
78
- }
79
- },
80
- };
81
- }
package/src/severity.ts DELETED
@@ -1,22 +0,0 @@
1
- import type { OsvVulnerability } from './client';
2
- import { PREFERRED_REF_TYPES } from './config';
3
-
4
- export function severityLevel(vuln: OsvVulnerability): 'fatal' | 'warn' {
5
- const s = vuln.database_specific?.severity?.toUpperCase();
6
- if (s === 'CRITICAL' || s === 'HIGH') return 'fatal';
7
- if (s === 'MODERATE' || s === 'LOW') return 'warn';
8
-
9
- // Fallback: numeric CVSS score (โ‰ฅ7.0 = HIGH/CRITICAL threshold).
10
- const score = vuln.database_specific?.cvss?.score;
11
- if (typeof score === 'number') return score >= 7.0 ? 'fatal' : 'warn';
12
-
13
- return 'warn';
14
- }
15
-
16
- export function advisoryUrl(vuln: OsvVulnerability): string {
17
- for (const type of PREFERRED_REF_TYPES) {
18
- const ref = vuln.references?.find((r) => r.type === type);
19
- if (ref) return ref.url;
20
- }
21
- return `https://osv.dev/vulnerability/${vuln.id}`;
22
- }
@@ -1,137 +0,0 @@
1
- import {
2
- CONCURRENCY,
3
- FETCH_TIMEOUT_MS,
4
- RATE_LIMIT,
5
- SNYK_API_BASE,
6
- SNYK_API_VERSION,
7
- SNYK_ORG_ID,
8
- SNYK_TOKEN,
9
- } from './config';
10
-
11
- export interface SnykIssue {
12
- id: string;
13
- attributes: {
14
- title: string;
15
- type: string;
16
- effective_severity_level: 'critical' | 'high' | 'medium' | 'low';
17
- description?: string;
18
- problems?: Array<{ id: string; source: string }>;
19
- };
20
- }
21
-
22
- interface SnykResponse {
23
- data: SnykIssue[];
24
- links?: { next?: string };
25
- }
26
-
27
- // Sliding window rate limiter โ€” tracks request timestamps within the last minute.
28
- class RateLimiter {
29
- private readonly timestamps: number[] = [];
30
- private readonly windowMs = 60_000;
31
-
32
- constructor(private readonly limit: number) {}
33
-
34
- async acquire(): Promise<void> {
35
- const now = Date.now();
36
- while (
37
- this.timestamps.length > 0 &&
38
- now - this.timestamps[0] >= this.windowMs
39
- ) {
40
- this.timestamps.shift();
41
- }
42
- if (this.timestamps.length < this.limit) {
43
- this.timestamps.push(Date.now());
44
- return;
45
- }
46
- // Wait until the oldest request falls outside the window, then retry.
47
- const waitMs = this.windowMs - (Date.now() - this.timestamps[0]) + 10;
48
- await Bun.sleep(waitMs);
49
- return this.acquire();
50
- }
51
- }
52
-
53
- const rateLimiter = new RateLimiter(RATE_LIMIT);
54
-
55
- function fetchWithTimeout(
56
- url: string,
57
- options?: RequestInit
58
- ): Promise<Response> {
59
- const controller = new AbortController();
60
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
61
- return fetch(url, { ...options, signal: controller.signal }).finally(() =>
62
- clearTimeout(timer)
63
- );
64
- }
65
-
66
- export function validateConfig(): void {
67
- if (!SNYK_TOKEN)
68
- throw new Error('SNYK_TOKEN is required for the Snyk scanner');
69
- if (!SNYK_ORG_ID)
70
- throw new Error('SNYK_ORG_ID is required for the Snyk scanner');
71
- }
72
-
73
- function buildPurl(name: string, version: string): string {
74
- // Scoped packages: @scope/name -> pkg:npm/scope/name@version (PURL spec ยง7)
75
- if (name.startsWith('@')) {
76
- const slash = name.indexOf('/', 1);
77
- const scope = name.slice(1, slash);
78
- const pkg = name.slice(slash + 1);
79
- return `pkg:npm/${scope}/${pkg}@${version}`;
80
- }
81
- return `pkg:npm/${name}@${version}`;
82
- }
83
-
84
- async function fetchPackageIssues(
85
- name: string,
86
- version: string,
87
- retries = 3
88
- ): Promise<SnykIssue[]> {
89
- await rateLimiter.acquire();
90
-
91
- const purl = encodeURIComponent(buildPurl(name, version));
92
- const url = `${SNYK_API_BASE}/orgs/${SNYK_ORG_ID}/packages/${purl}/issues?version=${SNYK_API_VERSION}&limit=1000`;
93
-
94
- const res = await fetchWithTimeout(url, {
95
- headers: {
96
- Authorization: `token ${SNYK_TOKEN}`,
97
- 'Content-Type': 'application/vnd.api+json',
98
- },
99
- });
100
-
101
- if (res.status === 429 && retries > 0) {
102
- const retryAfter = res.headers.get('Retry-After');
103
- const waitMs = retryAfter ? Number(retryAfter) * 1000 : 60_000;
104
- await Bun.sleep(waitMs);
105
- return fetchPackageIssues(name, version, retries - 1);
106
- }
107
-
108
- if (res.status === 404) return [];
109
-
110
- if (!res.ok) {
111
- const body = await res.text().catch(() => '');
112
- throw new Error(`Snyk API ${res.status}: ${body || res.statusText}`);
113
- }
114
-
115
- const { data } = (await res.json()) as SnykResponse;
116
- return data ?? [];
117
- }
118
-
119
- export async function batchFetchIssues(
120
- packages: Bun.Security.Package[],
121
- onProgress?: (completed: number, total: number) => void
122
- ): Promise<Map<string, SnykIssue[]>> {
123
- const results = new Map<string, SnykIssue[]>();
124
- let completed = 0;
125
-
126
- for (let i = 0; i < packages.length; i += CONCURRENCY) {
127
- await Promise.all(
128
- packages.slice(i, i + CONCURRENCY).map(async (pkg) => {
129
- const issues = await fetchPackageIssues(pkg.name, pkg.version);
130
- results.set(`${pkg.name}@${pkg.version}`, issues);
131
- onProgress?.(++completed, packages.length);
132
- })
133
- );
134
- }
135
-
136
- return results;
137
- }
@@ -1,15 +0,0 @@
1
- export const SNYK_API_BASE =
2
- Bun.env.SNYK_API_BASE ?? 'https://api.snyk.io/rest';
3
- export const SNYK_API_VERSION = Bun.env.SNYK_API_VERSION ?? '2024-04-29';
4
- export const SNYK_TOKEN = Bun.env.SNYK_TOKEN;
5
- export const SNYK_ORG_ID = Bun.env.SNYK_ORG_ID;
6
- export const FETCH_TIMEOUT_MS = Number(Bun.env.SNYK_TIMEOUT_MS) || 10_000;
7
- export const FAIL_CLOSED = Bun.env.SNYK_FAIL_CLOSED === 'true';
8
- export const NO_CACHE = Bun.env.SNYK_NO_CACHE === 'true';
9
- // Max concurrent connections (independent of rate limit)
10
- export const CONCURRENCY = Number(Bun.env.SNYK_CONCURRENCY) || 10;
11
- // Requests per minute โ€” hard ceiling is 180; default leaves headroom
12
- export const RATE_LIMIT = Math.min(Number(Bun.env.SNYK_RATE_LIMIT) || 160, 180);
13
-
14
- export const CACHE_FILE = Bun.env.SNYK_CACHE_FILE ?? '.snyk.lock';
15
- export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
package/src/snyk/index.ts DELETED
@@ -1,38 +0,0 @@
1
- import { type Backend, createScanner } from '../scanner';
2
- import { batchFetchIssues, validateConfig } from './client';
3
- import { CACHE_FILE, FAIL_CLOSED, NO_CACHE } from './config';
4
- import { advisoryUrl, severityLevel } from './severity';
5
-
6
- const backend: Backend = {
7
- name: 'Snyk',
8
- cacheFile: CACHE_FILE,
9
- noCache: NO_CACHE,
10
- failClosed: FAIL_CLOSED,
11
- validateConfig,
12
-
13
- async fetchAdvisories(packages, onStatus) {
14
- const issueMap = await batchFetchIssues(packages, (done, total) => {
15
- onStatus(`Scanning packages via Snyk (${done}/${total})...`);
16
- });
17
-
18
- const result = new Map<string, Bun.Security.Advisory[]>();
19
- for (const pkg of packages) {
20
- const key = `${pkg.name}@${pkg.version}`;
21
- const issues = issueMap.get(key) ?? [];
22
- result.set(
23
- key,
24
- issues.map((issue) => ({
25
- level: severityLevel(issue),
26
- package: pkg.name,
27
- url: advisoryUrl(issue),
28
- description: issue.attributes.title,
29
- }))
30
- );
31
- }
32
-
33
- return result;
34
- },
35
- };
36
-
37
- export { backend };
38
- export const scanner = createScanner(backend);
@@ -1,10 +0,0 @@
1
- import type { SnykIssue } from './client';
2
-
3
- export function severityLevel(issue: SnykIssue): 'fatal' | 'warn' {
4
- const level = issue.attributes.effective_severity_level;
5
- return level === 'critical' || level === 'high' ? 'fatal' : 'warn';
6
- }
7
-
8
- export function advisoryUrl(issue: SnykIssue): string {
9
- return `https://security.snyk.io/vuln/${issue.id}`;
10
- }