Skip to content

Commit 72d08e9

Browse files
committed
general repo maintenance
1 parent 9aecd9b commit 72d08e9

File tree

5 files changed

+108
-84
lines changed

5 files changed

+108
-84
lines changed

.github/workflows/test.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ If your `scan` function throws an error, it will be gracefully handled by Bun, b
3838

3939
### Validation
4040

41-
The template implements simple validation for the sake of keeping the template
42-
simple. Consider using a schema validation library like Zod for production:
41+
If you fetch a threat feed over the network, perhaps from your own API, consider
42+
using a schema validation library like Zod for production. This code needs to be
43+
defensive in all cases, so we should fail if we receive an invalid thread feed,
44+
rather than continuining and potentially returning an empty array of advisories.
45+
It's better to fail fast here.
4346

4447
```typescript
4548
import { z } from 'zod';
@@ -53,6 +56,22 @@ const ThreatFeedItemSchema = z.object({
5356
});
5457
```
5558

59+
### Useful Bun APIs
60+
61+
Bun provides several built-in APIs that are particularly useful for security providers:
62+
63+
- **`Bun.semver.satisfies()`**: Essential for checking if package versions match vulnerability ranges. No external dependencies needed.
64+
65+
```typescript
66+
if (Bun.semver.satisfies(version, '>=1.0.0 <1.2.5')) {
67+
// Version is vulnerable
68+
}
69+
```
70+
71+
- **`Bun.hash()`**: Fast hashing for package integrity checks
72+
- **`Bun.file()`**: Efficient file I/O for reading local threat databases
73+
- **`Bun.spawn()`**: Run external security scanners if needed
74+
5675
## Testing
5776

5877
This template comes with a test file already setup. It tests for a known

provider.test.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
import { expect, test } from 'bun:test';
2-
import { provider } from './src';
2+
import { provider } from './src/index.ts';
33

4-
const mockInstallInfo: Bun.Security.Package[] = [
5-
{
6-
name: 'bun',
7-
version: '1.3.0',
8-
requestedRange: '1.3.0',
9-
tarball: 'https://registry.npmjs.org/bun/-/bun-1.3.0.tgz',
10-
},
11-
{
12-
name: 'event-stream',
13-
version: '3.3.6', // This was a known incident in 2018 - https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident
14-
requestedRange: '3.3.6',
15-
tarball: 'https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz',
16-
},
17-
];
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+
/////////////////////////////////////////////////////////////////////////////////////
189

1910
test('Provider should warn about known malicious packages', async () => {
20-
const advisories = await provider.scan({ packages: mockInstallInfo });
11+
const advisories = await provider.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+
});
2121

2222
expect(advisories.length).toBeGreaterThan(0);
2323
const advisory = advisories[0]!;
24+
expect(advisory).toBeDefined();
2425

2526
expect(advisory).toMatchObject({
26-
level: 'warn',
27+
level: 'fatal',
2728
package: 'event-stream',
2829
url: expect.any(String),
2930
description: expect.any(String),
3031
});
3132
});
33+
34+
test('There should be no advisories if no packages are being installed', async () => {
35+
const advisories = await provider.scan({ packages: [] });
36+
expect(advisories.length).toBe(0);
37+
});

src/index.ts

Lines changed: 38 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,56 @@
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', // You can use Bun.semver.satisfies to match this
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+
return myPretendThreatFeed.filter(item => {
27+
return packages.some(
28+
p => p.name === item.package && Bun.semver.satisfies(p.version, item.range)
29+
);
30+
});
31+
}
32+
133
export const provider: Bun.Security.Provider = {
234
version: '1',
335
async scan({ packages }) {
4-
const response = await fetch('https://api.example.com/scan', {
5-
method: 'POST',
6-
body: JSON.stringify({
7-
packages: packages.map(p => ({
8-
name: p.name,
9-
version: p.version,
10-
})),
11-
}),
12-
headers: {
13-
'Content-Type': 'application/json',
14-
},
15-
});
16-
17-
const json = await response.json();
18-
validateThreatFeed(json);
36+
const feed = await fetchThreatFeed(packages);
1937

2038
// Iterate over reported threats and return an array of advisories. This
2139
// could be longer, shorter or equal length of the input packages array.
2240
// Whatever you return will be shown to the user.
2341

2442
const results: Bun.Security.Advisory[] = [];
2543

26-
for (const item of json) {
44+
for (const item of feed) {
2745
// Advisory levels control installation behavior:
2846
// - All advisories are always shown to the user regardless of level
2947
// - Fatal: Installation stops immediately (e.g., backdoors, botnets)
3048
// - Warning: User prompted in TTY, auto-cancelled in non-TTY (e.g., protestware, adware)
3149

32-
const isFatal = item.categories.includes('backdoor') || item.categories.includes('botnet');
50+
const isFatal =
51+
item.categories.includes('malware') ||
52+
item.categories.includes('backdoor') ||
53+
item.categories.includes('botnet');
3354

3455
const isWarning =
3556
item.categories.includes('protestware') || item.categories.includes('adware');
@@ -50,43 +71,3 @@ export const provider: Bun.Security.Provider = {
5071
return results;
5172
},
5273
};
53-
54-
type ThreatFeedItemCategory =
55-
| 'protestware'
56-
| 'adware'
57-
| 'backdoor'
58-
| 'botnet'; /* ...maybe you have some others */
59-
60-
interface ThreatFeedItem {
61-
package: string;
62-
version: string;
63-
url: string | null;
64-
description: string | null;
65-
categories: Array<ThreatFeedItemCategory>;
66-
}
67-
68-
// You should really use a schema validation library like Zod here to validate
69-
// the feed. This code needs to be defensive rather than fast, so it's sensible
70-
// to check just to be sure.
71-
function validateThreatFeed(json: unknown): asserts json is ThreatFeedItem[] {
72-
if (!Array.isArray(json)) {
73-
throw new Error('Invalid threat feed');
74-
}
75-
76-
for (const item of json) {
77-
if (
78-
typeof item !== 'object' ||
79-
item === null ||
80-
!('package' in item) ||
81-
!('version' in item) ||
82-
!('url' in item) ||
83-
!('description' in item) ||
84-
typeof item.package !== 'string' ||
85-
typeof item.version !== 'string' ||
86-
(typeof item.url !== 'string' && item.url !== null) ||
87-
(typeof item.description !== 'string' && item.description !== null)
88-
) {
89-
throw new Error('Invalid threat feed item');
90-
}
91-
}
92-
}

temp.d.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ declare module 'bun' {
3030
* This could be a tag like `beta` or a semver range like `>=4.0.0`
3131
*/
3232
requestedRange: string;
33-
34-
// /**
35-
// * Integrity hash provided from the registry
36-
// *
37-
// * Bun will usually verify this, but it's possible there are cases where
38-
// * it was not validated.
39-
// */
40-
// integrity: string;
4133
}
4234

4335
/**

0 commit comments

Comments
 (0)