Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/lib/frontend/handlers/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,27 @@ Future<shelf.Response> packageScoreHandler(
);
}

/// Handles requests for:
///
/// - `/packages/<package>/security`
/// - `/packages/<package>/versions/<version>/security`
Future<shelf.Response> packageSecurityHandler(
shelf.Request request,
String packageName, {
String? versionName,
}) async {
return await _handlePackagePage(
request: request,
packageName: packageName,
versionName: versionName,
assetKind: AssetKind.attestation,
requiresAdmin: false,
canonicalUrlFn: (p, v) => urls.pkgSecurityUrl(p, version: v),
renderFn: (data) => renderPkgSecurityPage(data),
cacheEntry: cache.uiPackageSecurity(packageName, versionName),
);
}

/// Handles requests for:
///
/// - `/packages/<package>/score/log.txt`
Expand Down
11 changes: 11 additions & 0 deletions app/lib/frontend/handlers/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ class PubSiteService {
String version,
) => packageScoreHandler(request, package, versionName: version);

@Route.get('/packages/<package>/versions/<version>/security')
Future<Response> packageVersionSecurity(
Request request,
String package,
String version,
) => packageSecurityHandler(request, package, versionName: version);

@Route.get('/packages/<package>/versions/<version>/score/log.txt')
Future<Response> packageVersionScoreLogTxt(
Request request,
Expand Down Expand Up @@ -181,6 +188,10 @@ class PubSiteService {
Future<Response> packageScore(Request request, String package) =>
packageScoreHandler(request, package);

@Route.get('/packages/<package>/security')
Future<Response> packageSecurity(Request request, String package) =>
packageSecurityHandler(request, package);

@Route.get('/packages/<package>/score/log.txt')
Future<Response> packageScoreLogTxt(Request request, String package) =>
packageScoreLogTxtHandler(request, package);
Expand Down
6 changes: 6 additions & 0 deletions app/lib/frontend/handlers/routes.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions app/lib/frontend/templates/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'views/pkg/header.dart';
import 'views/pkg/info_box.dart';
import 'views/pkg/install_tab.dart';
import 'views/pkg/score_tab.dart';
import 'views/pkg/security_tab.dart';
import 'views/pkg/title_content.dart';

/// Renders the right-side info box (quick summary of the package, mostly coming
Expand Down Expand Up @@ -132,6 +133,7 @@ d.Node renderPkgHeader(PackagePageData data) {
titleNode: titleContentNode(
package: package.name!,
version: data.version.version!,
slsaLevel: data.version.slsaLevel,
),
likeNode: renderLikeButtonAndLabel(
package: package.name!,
Expand Down Expand Up @@ -206,6 +208,15 @@ String renderPkgScorePage(PackagePageData data) {
return _renderPkgPage(data: data, tab: .score, tabContent: _scoreTab(data));
}

/// Renders the package security page.
String renderPkgSecurityPage(PackagePageData data) {
return _renderPkgPage(
data: data,
tab: .security,
tabContent: _securityTab(data),
);
}

final _pageTitleTabIdentifiers = <urls.PkgPageTab, String>{
urls.PkgPageTab.changelog: 'changelog',
urls.PkgPageTab.example: 'example',
Expand All @@ -216,6 +227,7 @@ final _pageTitleTabIdentifiers = <urls.PkgPageTab, String>{
urls.PkgPageTab.admin: 'admin',
urls.PkgPageTab.activityLog: 'activity log',
urls.PkgPageTab.versions: 'versions',
urls.PkgPageTab.security: 'security',
};

String _renderPkgPage({
Expand Down Expand Up @@ -493,6 +505,14 @@ Tab _scoreTab(PackagePageData data) {
);
}

Tab _securityTab(PackagePageData data) {
return Tab.withContent(
id: 'security',
title: 'Security',
contentNode: securityTabNode(data),
);
}

d.Node renderPackageSchemaOrgHtml(PackagePageData data) {
final p = data.package;
final pv = data.version;
Expand Down Expand Up @@ -573,6 +593,14 @@ List<Tab> buildPackageTabs({
title: 'Scores',
href: urls.pkgScoreUrl(package.name!, version: linkVersion),
);
final hasSecurity = data.version.slsaLevel != null || data.hasAttestation;
final securityTab =
contentIf(.security) ??
Tab.withLink(
id: 'security',
title: 'Security',
href: urls.pkgSecurityUrl(package.name!, version: linkVersion),
);
final adminTab =
contentIf(.admin) ??
Tab.withLink(
Expand All @@ -596,6 +624,7 @@ List<Tab> buildPackageTabs({
if (tab == .pubspec) content,
versionsTab,
scoreTab,
if (hasSecurity || tab == .security) securityTab,
if (data.isAdmin) adminTab,
if (data.isAdmin) activityLogTab,
];
Expand Down
25 changes: 25 additions & 0 deletions app/lib/frontend/templates/views/pkg/badge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,28 @@ d.Node packageBadgeNode({
],
);
}

/// Renders a shield badge with the SLSA provenance level (e.g. SLSA 2).
d.Node slsaShieldBadgeNode(int level, {String? href}) {
final content = [
d.unsafeRawHtml(
'<svg class="slsa-shield-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14" style="vertical-align: -2px; margin-right: 4px;"><path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3z" fill="#0175C2"/><text x="12" y="15.5" font-family="Roboto, sans-serif" font-size="11" font-weight="bold" text-anchor="middle" fill="#FFFFFF">$level</text></svg>',
),
d.text('SLSA $level'),
];
final title =
'Package provenance is verified with SLSA Level $level attestation.';
if (href != null) {
return d.a(
href: href,
classes: ['package-badge', 'package-badge-slsa'],
attributes: {'title': title},
children: content,
);
}
return d.span(
classes: ['package-badge', 'package-badge-slsa'],
attributes: {'title': title},
children: content,
);
}
22 changes: 22 additions & 0 deletions app/lib/frontend/templates/views/pkg/info_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import '../../../../service/topics/models.dart';
import '../../../../shared/urls.dart' as urls;
import '../../../dom/dom.dart' as d;
import '../shared/images.dart';
import 'badge.dart';
import 'screenshots.dart';

/// Links inside the package info box.
Expand Down Expand Up @@ -110,6 +111,7 @@ d.Node packageInfoBoxNode({
]),
),
if (license != null) _block('License', license),
if (version.slsaLevel != null) _provenance(data),
if (dependencies != null) _block('Dependencies', dependencies),
_more(
package.name!,
Expand Down Expand Up @@ -196,6 +198,26 @@ d.Node _more(String packageName, {required bool showImplementsLink}) {
);
}

d.Node _provenance(PackagePageData data) {
final slsaLevel = data.version.slsaLevel!;
final url = urls.pkgSecurityUrl(
data.package.name!,
version: data.isLatestStable ? null : data.version.version,
);
return _block(
'Provenance',
d.fragment([
slsaShieldBadgeNode(slsaLevel, href: url),
d.text(' '),
d.a(
href: url,
text: 'View details',
title: 'View build provenance details on the Security tab',
),
]),
);
}

d.Node _block(String title, d.Node? content) {
return d.fragment([
d.h3(classes: ['title'], text: title),
Expand Down
Loading
Loading