@involvex/bun-scanner 1.0.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/.github/workflows/test.yml +26 -0
- package/.prettierrc +11 -0
- package/README.md +108 -0
- package/package.json +25 -0
- package/scanner.test.ts +115 -0
- package/src/index.ts +74 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout code
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Bun
|
|
18
|
+
uses: oven-sh/setup-bun@v2
|
|
19
|
+
with:
|
|
20
|
+
bun-version: latest
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: bun install
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: bun test
|
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<img src="https://bun.com/logo.png" height="36" />
|
|
2
|
+
|
|
3
|
+
# Bun Security Scanner Template
|
|
4
|
+
|
|
5
|
+
A template for creating a security scanner for Bun's package installation
|
|
6
|
+
process. Security scanners scan packages against your threat intelligence feeds
|
|
7
|
+
and control whether installations proceed based on detected threats.
|
|
8
|
+
|
|
9
|
+
📚 [**Full documentation**](https://bun.com/docs/install/security-scanner-api)
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
When packages are installed via Bun, your security scanner:
|
|
14
|
+
|
|
15
|
+
1. **Receives** package information (name, version)
|
|
16
|
+
2. **Queries** your threat intelligence API
|
|
17
|
+
3. **Validates** the response data
|
|
18
|
+
4. **Categorizes** threats by severity
|
|
19
|
+
5. **Returns** advisories to control installation (empty array if safe)
|
|
20
|
+
|
|
21
|
+
### Advisory Levels
|
|
22
|
+
|
|
23
|
+
- **Fatal** (`level: 'fatal'`): Installation stops immediately
|
|
24
|
+
- Examples: malware, token stealers, backdoors, critical vulnerabilities
|
|
25
|
+
- **Warning** (`level: 'warn'`): User prompted for confirmation
|
|
26
|
+
- In TTY: User can choose to continue or cancel
|
|
27
|
+
- Non-TTY: Installation automatically cancelled
|
|
28
|
+
- Examples: protestware, adware, deprecated packages
|
|
29
|
+
|
|
30
|
+
All advisories are always displayed to the user regardless of level.
|
|
31
|
+
|
|
32
|
+
### Error Handling
|
|
33
|
+
|
|
34
|
+
If your `scan` function throws an error, it will be gracefully handled by Bun, but the installation process **will be cancelled** as a defensive precaution.
|
|
35
|
+
|
|
36
|
+
### Validation
|
|
37
|
+
|
|
38
|
+
When fetching threat feeds over the network, use schema validation
|
|
39
|
+
(e.g., Zod) to ensure data integrity. Invalid responses should fail immediately
|
|
40
|
+
rather than silently returning empty advisories.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import {z} from 'zod';
|
|
44
|
+
|
|
45
|
+
const ThreatFeedItemSchema = z.object({
|
|
46
|
+
package: z.string(),
|
|
47
|
+
version: z.string(),
|
|
48
|
+
url: z.string().nullable(),
|
|
49
|
+
description: z.string().nullable(),
|
|
50
|
+
categories: z.array(z.enum(['backdoor', 'botnet' /* ... */])),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Useful Bun APIs
|
|
55
|
+
|
|
56
|
+
Bun provides several built-in APIs that are particularly useful for security scanner:
|
|
57
|
+
|
|
58
|
+
- [**Security scanner API Reference**](https://bun.com/docs/install/security-scanner-api): Complete API documentation for security scanners
|
|
59
|
+
- [**`Bun.semver.satisfies()`**](https://bun.com/docs/api/semver): Essential for checking if package versions match vulnerability ranges. No external dependencies needed.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
if (Bun.semver.satisfies(version, '>=1.0.0 <1.2.5')) {
|
|
63
|
+
// Version is vulnerable
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- [**`Bun.hash`**](https://bun.com/docs/api/hashing#bun-hash): Fast hashing for package integrity checks
|
|
68
|
+
- [**`Bun.file`**](https://bun.com/docs/api/file-io): Efficient file I/O, could be used for reading local threat databases
|
|
69
|
+
|
|
70
|
+
## Testing
|
|
71
|
+
|
|
72
|
+
This template includes tests for a known malicious package version.
|
|
73
|
+
Customize the test file as needed.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun test
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Publishing Your Provider
|
|
80
|
+
|
|
81
|
+
Publish your security scanner to npm:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun publish
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Users can now install your provider and add it to their `bunfig.toml` configuration.
|
|
88
|
+
|
|
89
|
+
To test locally before publishing, use [`bun link`](https://bun.sh/docs/cli/link):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# In your provider directory
|
|
93
|
+
bun link
|
|
94
|
+
|
|
95
|
+
# In your test project
|
|
96
|
+
bun link @acme/bun # this is the name in package.json of your provider
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
This is a template repository. Fork it and customize for your organization's
|
|
102
|
+
security requirements.
|
|
103
|
+
|
|
104
|
+
## Support
|
|
105
|
+
|
|
106
|
+
For docs and questions, see the [Bun documentation](https://bun.com/docs/install/security-scanner-api) or [Join our Discord](https://bun.com/discord).
|
|
107
|
+
|
|
108
|
+
For template issues, please open an issue in this repository.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@involvex/bun-scanner",
|
|
3
|
+
"description": "Bun security scanner ",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "involvex",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/involvex/involvex-bun-scan"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"bun",
|
|
17
|
+
"security",
|
|
18
|
+
"provider"
|
|
19
|
+
],
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "^1.3.5",
|
|
22
|
+
"prettier": "^3.7.4",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/scanner.test.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test';
|
|
2
|
+
import {scanner} from './src/index.ts';
|
|
3
|
+
|
|
4
|
+
/////////////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
// This test file is mostly just here to get you up and running quickly. It's
|
|
6
|
+
// likely you'd want to improve or remove this, and add more coverage for your
|
|
7
|
+
// own code.
|
|
8
|
+
/////////////////////////////////////////////////////////////////////////////////////
|
|
9
|
+
|
|
10
|
+
test('Scanner should warn about known malicious packages', async () => {
|
|
11
|
+
const advisories = await scanner.scan({
|
|
12
|
+
packages: [
|
|
13
|
+
{
|
|
14
|
+
name: 'event-stream',
|
|
15
|
+
version: '3.3.6', // This was a known incident in 2018 - https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident
|
|
16
|
+
requestedRange: '^3.3.0',
|
|
17
|
+
tarball: 'https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(advisories.length).toBeGreaterThan(0);
|
|
23
|
+
const advisory = advisories[0]!;
|
|
24
|
+
expect(advisory).toBeDefined();
|
|
25
|
+
|
|
26
|
+
expect(advisory).toMatchObject({
|
|
27
|
+
level: 'fatal',
|
|
28
|
+
package: 'event-stream',
|
|
29
|
+
url: expect.any(String),
|
|
30
|
+
description: expect.any(String),
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('There should be no advisories if no packages are being installed', async () => {
|
|
35
|
+
const advisories = await scanner.scan({packages: []});
|
|
36
|
+
expect(advisories.length).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Safe packages should return no advisories', async () => {
|
|
40
|
+
const advisories = await scanner.scan({
|
|
41
|
+
packages: [
|
|
42
|
+
{
|
|
43
|
+
name: 'lodash',
|
|
44
|
+
version: '4.17.21',
|
|
45
|
+
requestedRange: '^4.17.0',
|
|
46
|
+
tarball: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
expect(advisories.length).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Should handle multiple packages with mixed security status', async () => {
|
|
54
|
+
const advisories = await scanner.scan({
|
|
55
|
+
packages: [
|
|
56
|
+
{
|
|
57
|
+
name: 'event-stream',
|
|
58
|
+
version: '3.3.6', // malicious
|
|
59
|
+
requestedRange: '^3.3.0',
|
|
60
|
+
tarball: 'https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'lodash',
|
|
64
|
+
version: '4.17.21', // safe
|
|
65
|
+
requestedRange: '^4.17.0',
|
|
66
|
+
tarball: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(advisories.length).toBe(1);
|
|
72
|
+
expect(advisories[0]?.package).toBe('event-stream');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Should differentiate between versions of the same package', async () => {
|
|
76
|
+
const maliciousVersion = await scanner.scan({
|
|
77
|
+
packages: [
|
|
78
|
+
{
|
|
79
|
+
name: 'event-stream',
|
|
80
|
+
version: '3.3.6', // malicious version
|
|
81
|
+
requestedRange: '3.3.6',
|
|
82
|
+
tarball: 'https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const safeVersion = await scanner.scan({
|
|
88
|
+
packages: [
|
|
89
|
+
{
|
|
90
|
+
name: 'event-stream',
|
|
91
|
+
version: '4.0.0', // safe version
|
|
92
|
+
requestedRange: '4.0.0',
|
|
93
|
+
tarball: 'https://registry.npmjs.org/event-stream/-/event-stream-4.0.0.tgz',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(maliciousVersion.length).toBeGreaterThan(0);
|
|
99
|
+
expect(safeVersion.length).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Should handle scoped packages correctly', async () => {
|
|
103
|
+
const advisories = await scanner.scan({
|
|
104
|
+
packages: [
|
|
105
|
+
{
|
|
106
|
+
name: '@types/node',
|
|
107
|
+
version: '20.0.0',
|
|
108
|
+
requestedRange: '^20.0.0',
|
|
109
|
+
tarball: 'https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(advisories.length).toBe(0);
|
|
115
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// This is just an example interface of mock data. You can change this to the
|
|
2
|
+
// type of your actual threat feed (or ideally use a good schema validation
|
|
3
|
+
// library to infer your types from).
|
|
4
|
+
interface ThreatFeedItem {
|
|
5
|
+
package: string;
|
|
6
|
+
range: string;
|
|
7
|
+
url: string | null;
|
|
8
|
+
description: string | null;
|
|
9
|
+
categories: Array<'protestware' | 'adware' | 'backdoor' | 'malware' | 'botnet'>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function fetchThreatFeed(packages: Bun.Security.Package[]): Promise<ThreatFeedItem[]> {
|
|
13
|
+
// In a real provider you would probably replace this mock data with a
|
|
14
|
+
// fetch() to your threat feed, validating it with Zod or a similar library.
|
|
15
|
+
|
|
16
|
+
const myPretendThreatFeed: ThreatFeedItem[] = [
|
|
17
|
+
{
|
|
18
|
+
package: 'event-stream',
|
|
19
|
+
range: '>=3.3.6 <4.0.0', // Matches 3.3.6 and above but less than 4.0.0
|
|
20
|
+
url: 'https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident',
|
|
21
|
+
description: 'event-stream is a malicious package',
|
|
22
|
+
categories: ['malware'],
|
|
23
|
+
},
|
|
24
|
+
// ...
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
return myPretendThreatFeed.filter(item => {
|
|
28
|
+
return packages.some(
|
|
29
|
+
p => p.name === item.package && Bun.semver.satisfies(p.version, item.range),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const scanner: Bun.Security.Scanner = {
|
|
35
|
+
version: '1', // This is the version of Bun security scanner implementation. You should keep this set as '1'
|
|
36
|
+
async scan({packages}) {
|
|
37
|
+
const feed = await fetchThreatFeed(packages);
|
|
38
|
+
|
|
39
|
+
// Iterate over reported threats and return an array of advisories. This
|
|
40
|
+
// could be longer, shorter or equal length of the input packages array.
|
|
41
|
+
// Whatever you return will be shown to the user.
|
|
42
|
+
|
|
43
|
+
const results: Bun.Security.Advisory[] = [];
|
|
44
|
+
|
|
45
|
+
for (const item of feed) {
|
|
46
|
+
// Advisory levels control installation behavior:
|
|
47
|
+
// - All advisories are always shown to the user regardless of level
|
|
48
|
+
// - Fatal: Installation stops immediately (e.g., backdoors, botnets)
|
|
49
|
+
// - Warning: User prompted in TTY, auto-cancelled in non-TTY (e.g., protestware, adware)
|
|
50
|
+
|
|
51
|
+
const isFatal =
|
|
52
|
+
item.categories.includes('malware') ||
|
|
53
|
+
item.categories.includes('backdoor') ||
|
|
54
|
+
item.categories.includes('botnet');
|
|
55
|
+
|
|
56
|
+
const isWarning =
|
|
57
|
+
item.categories.includes('protestware') || item.categories.includes('adware');
|
|
58
|
+
|
|
59
|
+
if (!isFatal && !isWarning) continue;
|
|
60
|
+
|
|
61
|
+
// Besides the .level property, the other properties are just here
|
|
62
|
+
// for display to the user.
|
|
63
|
+
results.push({
|
|
64
|
+
level: isFatal ? 'fatal' : 'warn',
|
|
65
|
+
package: item.package,
|
|
66
|
+
url: item.url,
|
|
67
|
+
description: item.description,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Return an empty array if there are no advisories!
|
|
72
|
+
return results;
|
|
73
|
+
},
|
|
74
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"noUnusedLocals": false,
|
|
19
|
+
"noUnusedParameters": false,
|
|
20
|
+
"noPropertyAccessFromIndexSignature": false
|
|
21
|
+
}
|
|
22
|
+
}
|