Skip to content

Commit 4aa6fdf

Browse files
committed
Initial tab at a security tab showing advisories
1 parent 0a6ffe9 commit 4aa6fdf

File tree

8 files changed

+502
-0
lines changed

8 files changed

+502
-0
lines changed

app/components/crate-header.gjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ export default class CrateHeader extends Component {
106106
Dependents
107107
</nav.Tab>
108108

109+
<nav.Tab @link={{link_ 'crate.security' @crate}} data-test-security-tab>
110+
Security
111+
</nav.Tab>
112+
109113
{{#if this.isOwner}}
110114
<nav.Tab @link={{link_ 'crate.settings' @crate}} data-test-settings-tab>
111115
Settings

app/router.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Router.map(function () {
1818
this.route('range', { path: '/range/:range' });
1919

2020
this.route('reverse-dependencies', { path: 'reverse_dependencies' });
21+
this.route('security');
2122

2223
this.route('owners');
2324
this.route('settings', function () {

app/routes/crate/security.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Route from '@ember/routing/route';
2+
import { service } from '@ember/service';
3+
4+
async function fetchAdvisories(crateId) {
5+
let url = `https://rustsec.org/packages/${crateId}.json`;
6+
let response = await fetch(url);
7+
if (response.status === 404) {
8+
return [];
9+
} else if (response.ok) {
10+
return await response.json();
11+
} else {
12+
throw new Error(`HTTP error! status: ${response}`);
13+
}
14+
}
15+
16+
export default class SecurityRoute extends Route {
17+
@service sentry;
18+
19+
async model() {
20+
let crate = this.modelFor('crate');
21+
try {
22+
let [advisories, micromarkModule, gfmModule] = await Promise.all([
23+
fetchAdvisories(crate.id),
24+
import('micromark'),
25+
import('micromark-extension-gfm'),
26+
]);
27+
28+
const convertMarkdown = markdown => {
29+
return micromarkModule.micromark(markdown, {
30+
extensions: [gfmModule.gfm()],
31+
htmlExtensions: [gfmModule.gfmHtml()],
32+
});
33+
};
34+
35+
return { crate, advisories, convertMarkdown, error: false };
36+
} catch (error) {
37+
this.sentry.captureException(error);
38+
return { crate, advisories: [], convertMarkdown: null, error: true };
39+
}
40+
}
41+
42+
setupController(controller, { crate, advisories, convertMarkdown, error }) {
43+
super.setupController(...arguments);
44+
controller.crate = crate;
45+
controller.advisories = advisories;
46+
controller.convertMarkdown = convertMarkdown;
47+
controller.error = error;
48+
}
49+
}

app/templates/crate/security.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.heading {
2+
font-size: 1.17em;
3+
margin-block-start: 1em;
4+
margin-block-end: 1em;
5+
}
6+
7+
.advisories {
8+
list-style: none;
9+
margin: 0;
10+
padding: 0;
11+
}
12+
13+
.row {
14+
margin-top: var(--space-2xs);
15+
background-color: light-dark(white, #141413);
16+
border-radius: var(--space-3xs);
17+
padding: var(--space-m) var(--space-l);
18+
list-style: none;
19+
}
20+
21+
.no-results {
22+
padding: var(--space-l) var(--space-s);
23+
background-color: light-dark(white, #141413);
24+
text-align: center;
25+
font-size: 20px;
26+
font-weight: 300;
27+
overflow-wrap: break-word;
28+
line-height: 1.5;
29+
}

app/templates/crate/security.gjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { htmlSafe } from '@ember/template';
2+
3+
import CrateHeader from 'crates-io/components/crate-header';
4+
5+
<template>
6+
<CrateHeader @crate={{@controller.crate}} />
7+
{{#if @controller.advisories.length}}
8+
<h2 class='heading'>Advisories</h2>
9+
<ul class='advisories' data-test-list>
10+
{{#each @controller.advisories as |advisory|}}
11+
<li class='row'>
12+
<h3>
13+
<a href='https://rustsec.org/advisories/{{advisory.id}}.html'>{{advisory.id}}</a>:
14+
{{advisory.summary}}
15+
</h3>
16+
{{htmlSafe (@controller.convertMarkdown advisory.details)}}
17+
</li>
18+
{{/each}}
19+
</ul>
20+
{{else if @controller.error}}
21+
<div class='no-results' data-error>
22+
An error occurred while fetching advisories.
23+
</div>
24+
{{else}}
25+
<div class='no-results' data-no-advisories>
26+
No advisories found for this crate.
27+
</div>
28+
{{/if}}
29+
</template>

e2e/acceptance/security.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect, test } from '@/e2e/helper';
2+
import { http, HttpResponse } from 'msw';
3+
4+
test.describe('Acceptance | crate security page', { tag: '@acceptance' }, () => {
5+
test('show some advisories', async ({ page, msw, percy }) => {
6+
let crate = await msw.db.crate.create({ name: 'foo' });
7+
await msw.db.version.create({ crate, num: '1.0.0' });
8+
9+
let advisories = [
10+
{
11+
id: 'TEST-001',
12+
summary: 'First test advisory',
13+
details: 'This is the first test advisory with **markdown** support.',
14+
},
15+
{
16+
id: 'TEST-002',
17+
summary: 'Second test advisory',
18+
details: 'This is the second test advisory with more details.',
19+
},
20+
];
21+
22+
msw.worker.use(
23+
http.get('https://rustsec.org/packages/:crateId.json', () => HttpResponse.json(advisories)),
24+
);
25+
26+
await page.goto('/crates/foo/security');
27+
28+
await expect(page.locator('[data-test-list] li')).toHaveCount(2);
29+
30+
// Check first advisory
31+
await expect(page.locator('[data-test-list] li').nth(0).locator('h3 a')).toHaveAttribute(
32+
'href',
33+
'https://rustsec.org/advisories/TEST-001.html',
34+
);
35+
await expect(page.locator('[data-test-list] li').nth(0).locator('h3 a')).toContainText('TEST-001');
36+
await expect(page.locator('[data-test-list] li').nth(0).locator('h3')).toContainText('First test advisory');
37+
await expect(page.locator('[data-test-list] li').nth(0).locator('p')).toContainText('markdown');
38+
39+
// Check second advisory
40+
await expect(page.locator('[data-test-list] li').nth(1).locator('h3 a')).toHaveAttribute(
41+
'href',
42+
'https://rustsec.org/advisories/TEST-002.html',
43+
);
44+
await expect(page.locator('[data-test-list] li').nth(1).locator('h3 a')).toContainText('TEST-002');
45+
await expect(page.locator('[data-test-list] li').nth(1).locator('h3')).toContainText('Second test advisory');
46+
47+
await percy.snapshot();
48+
});
49+
50+
test('show no advisory data when none exist', async ({ page, msw }) => {
51+
let crate = await msw.db.crate.create({ name: 'safe-crate' });
52+
await msw.db.version.create({ crate, num: '1.0.0' });
53+
54+
msw.worker.use(
55+
http.get('https://rustsec.org/packages/:crateId.json', () => HttpResponse.text('not found', { status: 404 })),
56+
);
57+
58+
await page.goto('/crates/safe-crate/security');
59+
60+
await expect(page.locator('[data-no-advisories]')).toBeVisible();
61+
await expect(page.locator('[data-no-advisories]')).toHaveText('No advisories found for this crate.');
62+
});
63+
64+
test('handles errors gracefully', async ({ page, msw }) => {
65+
let crate = await msw.db.crate.create({ name: 'error-crate' });
66+
await msw.db.version.create({ crate, num: '1.0.0' });
67+
68+
msw.worker.use(
69+
http.get('https://rustsec.org/packages/:crateId.json', () =>
70+
HttpResponse.text('Internal Server Error', { status: 500 }),
71+
),
72+
);
73+
74+
await page.goto('/crates/error-crate/security');
75+
76+
// When there's an error, the route catches it and returns empty advisories
77+
await expect(page.locator('[data-error]')).toBeVisible();
78+
await expect(page.locator('[data-error]')).toHaveText('An error occurred while fetching advisories.');
79+
});
80+
});

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
"loader.js": "4.7.0",
134134
"match-json": "1.3.7",
135135
"memory-scroll": "2.0.1",
136+
"micromark": "4.0.2",
137+
"micromark-extension-gfm": "^3.0.0",
136138
"msw": "2.12.4",
137139
"playwright-msw": "3.0.1",
138140
"postcss": "8.5.6",

0 commit comments

Comments
 (0)