From f813b3ea8e68c8a46f3275b6ff91fcec7cd5c4e6 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 20 Aug 2026 09:19:29 +0000 Subject: [PATCH 1/6] Display SLSA provenance level shield badge next to package versions - Adds `slsaLevel` property to `PackageVersion` and `VersionInfo`. - In `backend.dart`, populates `slsaLevel` when an attestation bundle is verified during upload. - Adds `slsaShieldBadgeNode` in `views/pkg/badge.dart` rendering a shield icon with the SLSA level number. - Displays SLSA shield badge in package page title and version rows. - Styles `.package-badge-slsa` in `_pkg.scss`. --- app/lib/frontend/templates/package.dart | 1 + app/lib/frontend/templates/views/pkg/badge.dart | 17 +++++++++++++++++ .../templates/views/pkg/title_content.dart | 8 +++++++- .../views/pkg/versions/version_row.dart | 8 +++++++- app/lib/package/backend.dart | 6 +++++- app/lib/package/models.dart | 4 ++++ pkg/_pub_shared/lib/data/package_api.dart | 5 +++++ pkg/_pub_shared/lib/data/package_api.g.dart | 2 ++ pkg/web_css/lib/src/_pkg.scss | 6 ++++++ 9 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/lib/frontend/templates/package.dart b/app/lib/frontend/templates/package.dart index 4377475c02..1ab3dd754d 100644 --- a/app/lib/frontend/templates/package.dart +++ b/app/lib/frontend/templates/package.dart @@ -132,6 +132,7 @@ d.Node renderPkgHeader(PackagePageData data) { titleNode: titleContentNode( package: package.name!, version: data.version.version!, + slsaLevel: data.version.slsaLevel, ), likeNode: renderLikeButtonAndLabel( package: package.name!, diff --git a/app/lib/frontend/templates/views/pkg/badge.dart b/app/lib/frontend/templates/views/pkg/badge.dart index 4fdde69b3d..4d76aa9acc 100644 --- a/app/lib/frontend/templates/views/pkg/badge.dart +++ b/app/lib/frontend/templates/views/pkg/badge.dart @@ -19,3 +19,20 @@ d.Node packageBadgeNode({ ], ); } + +/// Renders a shield badge with the SLSA provenance level (e.g. SLSA 2). +d.Node slsaShieldBadgeNode(int level) { + return d.span( + classes: ['package-badge', 'package-badge-slsa'], + attributes: { + 'title': + 'Package provenance is verified with SLSA Level $level attestation.', + }, + children: [ + d.unsafeRawHtml( + '$level', + ), + d.text('SLSA $level'), + ], + ); +} diff --git a/app/lib/frontend/templates/views/pkg/title_content.dart b/app/lib/frontend/templates/views/pkg/title_content.dart index 5cf09ebfb1..2ecfd51893 100644 --- a/app/lib/frontend/templates/views/pkg/title_content.dart +++ b/app/lib/frontend/templates/views/pkg/title_content.dart @@ -4,11 +4,17 @@ import '../../../dom/dom.dart' as d; import '../../../static_files.dart'; +import 'badge.dart'; -d.Node titleContentNode({required String package, required String version}) { +d.Node titleContentNode({ + required String package, + required String version, + int? slsaLevel, +}) { return d.fragment([ d.text('$package $version '), copyIcon(package: package, version: version), + if (slsaLevel != null) ...[d.text(' '), slsaShieldBadgeNode(slsaLevel)], ]); } diff --git a/app/lib/frontend/templates/views/pkg/versions/version_row.dart b/app/lib/frontend/templates/views/pkg/versions/version_row.dart index b0e9cff84b..596cfe38c9 100644 --- a/app/lib/frontend/templates/views/pkg/versions/version_row.dart +++ b/app/lib/frontend/templates/views/pkg/versions/version_row.dart @@ -9,6 +9,7 @@ import '../../../../../package/model_properties.dart'; import '../../../../../shared/urls.dart' as urls; import '../../../../dom/dom.dart' as d; import '../../../../static_files.dart'; +import '../badge.dart'; d.Node versionRowNode( String package, @@ -28,7 +29,12 @@ d.Node versionRowNode( title: 'Visit $package ${version.version} page', ), ), - d.td(classes: ['badge'], child: null), + d.td( + classes: ['badge'], + child: version.slsaLevel != null + ? slsaShieldBadgeNode(version.slsaLevel!) + : null, + ), d.td( classes: ['sdk'], child: sdk != null diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index 145495eda2..acb135c821 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -2683,6 +2683,7 @@ extension PackageVersionExt on PackageVersion { ), archiveSha256: archiveSha256, published: created, + slsaLevel: slsaLevel, ); } } @@ -2886,7 +2887,10 @@ Future<_UploadEntities> _createUploadEntities( ..libraries = archive.libraries ..uploader = agent.agentId ..sha256 = sha256Hash - ..provenance = provenance; + ..provenance = provenance + // A verified build provenance from a trusted builder on a hosted runner + // is SLSA Build Level 2. + ..slsaLevel = provenance != null ? 2 : null; final derived = derivePackageVersionEntities( archive: archive, diff --git a/app/lib/package/models.dart b/app/lib/package/models.dart index 71c30a8744..16f74b6f62 100644 --- a/app/lib/package/models.dart +++ b/app/lib/package/models.dart @@ -698,6 +698,10 @@ class PackageVersion extends db.ExpandoModel { @db.DateTimeProperty() DateTime? retracted; + /// The SLSA provenance level (e.g. 2) if this version is attested. + @db.IntProperty() + int? slsaLevel; + /// `true` if package version was moderated (pending moderation or deletion). @db.BoolProperty(required: true) bool isModerated = false; diff --git a/pkg/_pub_shared/lib/data/package_api.dart b/pkg/_pub_shared/lib/data/package_api.dart index 52510db8f7..9bcae68715 100644 --- a/pkg/_pub_shared/lib/data/package_api.dart +++ b/pkg/_pub_shared/lib/data/package_api.dart @@ -278,6 +278,10 @@ class VersionInfo { /// This is an optional field of the API response, it may be `null` or omitted. final DateTime? published; + /// The SLSA provenance level if the version has an attestation (e.g. 2). + @JsonKey(name: 'slsa_level') + final int? slsaLevel; + VersionInfo({ required this.version, required this.retracted, @@ -285,6 +289,7 @@ class VersionInfo { required this.archiveUrl, required this.archiveSha256, required this.published, + this.slsaLevel, }); factory VersionInfo.fromJson(Map json) => diff --git a/pkg/_pub_shared/lib/data/package_api.g.dart b/pkg/_pub_shared/lib/data/package_api.g.dart index c69695d24b..39263d9917 100644 --- a/pkg/_pub_shared/lib/data/package_api.g.dart +++ b/pkg/_pub_shared/lib/data/package_api.g.dart @@ -179,6 +179,7 @@ VersionInfo _$VersionInfoFromJson(Map json) => VersionInfo( published: json['published'] == null ? null : DateTime.parse(json['published'] as String), + slsaLevel: (json['slsa_level'] as num?)?.toInt(), ); Map _$VersionInfoToJson(VersionInfo instance) => @@ -189,6 +190,7 @@ Map _$VersionInfoToJson(VersionInfo instance) => 'archive_url': ?instance.archiveUrl, 'archive_sha256': ?instance.archiveSha256, 'published': ?instance.published?.toIso8601String(), + 'slsa_level': ?instance.slsaLevel, }; VersionScore _$VersionScoreFromJson(Map json) => VersionScore( diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 886459250f..f97fd7cd9d 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -20,6 +20,12 @@ color: var(--pub-badge-red-color); } + &.package-badge-slsa { + border-color: var(--pub-badge-default-color); + color: var(--pub-badge-default-color); + font-weight: 500; + } + &.package-badge-name-match { position: relative; top: -5px; From fb262a1eefe5ea3c79cb3b4e3ba268ebdc02bd13 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 2 Sep 2026 10:35:53 +0000 Subject: [PATCH 2/6] Add Security tab for package build provenance and attestations - Implement OpenSSF Level AAA Security tab displaying source, build, attestation, and integrity details. - Parse Fulcio certificate extensions, Rekor log info, and package checksums from Sigstore bundle. - Support downloading raw Sigstore attestation bundle (.sigstore.json). - Render educational empty state for versions without build provenance. - Link SLSA shield badges in header and versions table directly to the Security tab. - Add routes for /packages//security and /packages//versions//security. --- app/lib/frontend/handlers/package.dart | 21 + app/lib/frontend/handlers/routes.dart | 11 + app/lib/frontend/handlers/routes.g.dart | 6 + app/lib/frontend/templates/package.dart | 28 + .../frontend/templates/views/pkg/badge.dart | 30 +- .../templates/views/pkg/security_tab.dart | 749 ++++++++++++++++++ .../templates/views/pkg/title_content.dart | 9 +- .../views/pkg/versions/version_row.dart | 5 +- app/lib/package/backend.dart | 1 + app/lib/package/models.dart | 1 + app/lib/shared/redis_cache.dart | 5 + app/lib/shared/urls.dart | 11 + app/test/frontend/handlers/package_test.dart | 38 + app/test/frontend/security_tab_test.dart | 224 ++++++ pkg/web_css/lib/src/_pkg.scss | 131 +++ 15 files changed, 1257 insertions(+), 13 deletions(-) create mode 100644 app/lib/frontend/templates/views/pkg/security_tab.dart create mode 100644 app/test/frontend/security_tab_test.dart diff --git a/app/lib/frontend/handlers/package.dart b/app/lib/frontend/handlers/package.dart index 52e7354c4f..ac54b98bbf 100644 --- a/app/lib/frontend/handlers/package.dart +++ b/app/lib/frontend/handlers/package.dart @@ -237,6 +237,27 @@ Future packageScoreHandler( ); } +/// Handles requests for: +/// +/// - `/packages//security` +/// - `/packages//versions//security` +Future 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//score/log.txt` diff --git a/app/lib/frontend/handlers/routes.dart b/app/lib/frontend/handlers/routes.dart index 00bc2c4e75..305b3ca602 100644 --- a/app/lib/frontend/handlers/routes.dart +++ b/app/lib/frontend/handlers/routes.dart @@ -131,6 +131,13 @@ class PubSiteService { String version, ) => packageScoreHandler(request, package, versionName: version); + @Route.get('/packages//versions//security') + Future packageVersionSecurity( + Request request, + String package, + String version, + ) => packageSecurityHandler(request, package, versionName: version); + @Route.get('/packages//versions//score/log.txt') Future packageVersionScoreLogTxt( Request request, @@ -181,6 +188,10 @@ class PubSiteService { Future packageScore(Request request, String package) => packageScoreHandler(request, package); + @Route.get('/packages//security') + Future packageSecurity(Request request, String package) => + packageSecurityHandler(request, package); + @Route.get('/packages//score/log.txt') Future packageScoreLogTxt(Request request, String package) => packageScoreLogTxtHandler(request, package); diff --git a/app/lib/frontend/handlers/routes.g.dart b/app/lib/frontend/handlers/routes.g.dart index 73572172d1..63ece8aeb5 100644 --- a/app/lib/frontend/handlers/routes.g.dart +++ b/app/lib/frontend/handlers/routes.g.dart @@ -49,6 +49,11 @@ Router _$PubSiteServiceRouter(PubSiteService service) { r'/packages//versions//score', service.packageVersionScore, ); + router.add( + 'GET', + r'/packages//versions//security', + service.packageVersionSecurity, + ); router.add( 'GET', r'/packages//versions//score/log.txt', @@ -72,6 +77,7 @@ Router _$PubSiteServiceRouter(PubSiteService service) { router.add('GET', r'/packages//publisher', service.packagePublisher); router.add('GET', r'/packages//pubspec', service.packagePubspec); router.add('GET', r'/packages//score', service.packageScore); + router.add('GET', r'/packages//security', service.packageSecurity); router.add( 'GET', r'/packages//score/log.txt', diff --git a/app/lib/frontend/templates/package.dart b/app/lib/frontend/templates/package.dart index 1ab3dd754d..2d319c09aa 100644 --- a/app/lib/frontend/templates/package.dart +++ b/app/lib/frontend/templates/package.dart @@ -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 @@ -207,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.changelog: 'changelog', urls.PkgPageTab.example: 'example', @@ -217,6 +227,7 @@ final _pageTitleTabIdentifiers = { urls.PkgPageTab.admin: 'admin', urls.PkgPageTab.activityLog: 'activity log', urls.PkgPageTab.versions: 'versions', + urls.PkgPageTab.security: 'security', }; String _renderPkgPage({ @@ -494,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; @@ -574,6 +593,14 @@ List 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( @@ -597,6 +624,7 @@ List buildPackageTabs({ if (tab == .pubspec) content, versionsTab, scoreTab, + if (hasSecurity || tab == .security) securityTab, if (data.isAdmin) adminTab, if (data.isAdmin) activityLogTab, ]; diff --git a/app/lib/frontend/templates/views/pkg/badge.dart b/app/lib/frontend/templates/views/pkg/badge.dart index 4d76aa9acc..a015a08880 100644 --- a/app/lib/frontend/templates/views/pkg/badge.dart +++ b/app/lib/frontend/templates/views/pkg/badge.dart @@ -21,18 +21,26 @@ d.Node packageBadgeNode({ } /// Renders a shield badge with the SLSA provenance level (e.g. SLSA 2). -d.Node slsaShieldBadgeNode(int level) { +d.Node slsaShieldBadgeNode(int level, {String? href}) { + final content = [ + d.unsafeRawHtml( + '$level', + ), + 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': - 'Package provenance is verified with SLSA Level $level attestation.', - }, - children: [ - d.unsafeRawHtml( - '$level', - ), - d.text('SLSA $level'), - ], + attributes: {'title': title}, + children: content, ); } diff --git a/app/lib/frontend/templates/views/pkg/security_tab.dart b/app/lib/frontend/templates/views/pkg/security_tab.dart new file mode 100644 index 0000000000..54d17f0ace --- /dev/null +++ b/app/lib/frontend/templates/views/pkg/security_tab.dart @@ -0,0 +1,749 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import '../../../../package/models.dart'; +import '../../../../shared/urls.dart' as urls; +import '../../../dom/dom.dart' as d; +import 'badge.dart'; + +/// Parsed provenance attestation information. +class AttestationDetails { + final int slsaLevel; + final String? repository; + final String? commit; + final String? ref; + final String? workflow; + final String? runUrl; + final String? signerIdentity; + final String? oidcIssuer; + final String? rekorLogIndex; + final String? rekorUrl; + final DateTime? rekorTimestamp; + final String? archiveSha256; + + AttestationDetails({ + required this.slsaLevel, + this.repository, + this.commit, + this.ref, + this.workflow, + this.runUrl, + this.signerIdentity, + this.oidcIssuer, + this.rekorLogIndex, + this.rekorUrl, + this.rekorTimestamp, + this.archiveSha256, + }); + + static AttestationDetails? parse({ + required PackagePageData data, + String? rawBundleJson, + }) { + final slsaLevel = data.version.slsaLevel ?? 2; + if (rawBundleJson == null || rawBundleJson.trim().isEmpty) { + if (data.version.slsaLevel != null) { + return AttestationDetails( + slsaLevel: slsaLevel, + repository: data.packageLinks.repositoryUrl, + ); + } + return null; + } + + try { + final bundle = jsonDecode(rawBundleJson) as Map; + + String? rekorLogIndex; + String? rekorUrl; + DateTime? rekorTimestamp; + String? archiveSha256; + String? repository; + String? commit; + String? ref; + String? workflow; + String? runUrl; + String? signerIdentity; + String? oidcIssuer; + + // 1. Transparency log (Rekor) entries + final vm = bundle['verificationMaterial'] as Map?; + if (vm != null) { + final tlogEntries = vm['tlogEntries'] as List?; + if (tlogEntries != null && tlogEntries.isNotEmpty) { + final first = tlogEntries.first as Map; + final indexVal = first['logIndex']; + if (indexVal != null) { + rekorLogIndex = indexVal.toString(); + rekorUrl = 'https://search.sigstore.dev/?logIndex=$rekorLogIndex'; + } + final timeVal = first['integratedTime']; + if (timeVal != null) { + final seconds = int.tryParse(timeVal.toString()); + if (seconds != null) { + rekorTimestamp = DateTime.fromMillisecondsSinceEpoch( + seconds * 1000, + isUtc: true, + ); + } + } + if (first['canonicalizedBody'] != null) { + try { + final bodyBytes = base64Decode( + first['canonicalizedBody'] as String, + ); + final bodyJson = + jsonDecode(utf8.decode(bodyBytes)) as Map; + final spec = bodyJson['spec'] as Map?; + final hashVal = spec?['data']?['hash']?['value'] as String?; + if (hashVal != null && hashVal.isNotEmpty) { + archiveSha256 = hashVal; + } + } catch (_) {} + } + } + + // 2. Certificate parsing (ASN.1 DER strings) + final cert = vm['certificate'] as Map?; + if (cert != null && cert['rawBytes'] is String) { + try { + final certBytes = base64Decode(cert['rawBytes'] as String); + final strings = _extractAsn1Strings(certBytes); + for (final s in strings) { + if (s.contains('github.com/') && + s.contains('/.github/workflows/')) { + signerIdentity ??= s; + final atIdx = s.lastIndexOf('@'); + final baseUri = atIdx != -1 ? s.substring(0, atIdx) : s; + if (atIdx != -1 && ref == null) { + final refPart = s.substring(atIdx + 1); + if (refPart.startsWith('refs/')) { + ref = refPart; + } + } + final wfIdx = baseUri.indexOf('/.github/workflows/'); + if (wfIdx != -1) { + repository ??= baseUri.substring(0, wfIdx); + workflow ??= baseUri.substring(wfIdx + 1); + } + } else if (commit == null && + s.length == 40 && + RegExp(r'^[0-9a-fA-F]{40}$').hasMatch(s)) { + commit = s; + } else if (runUrl == null && + s.contains('github.com/') && + s.contains('/actions/runs/')) { + runUrl = s; + } else if (oidcIssuer == null && + s.contains('token.actions.githubusercontent.com')) { + oidcIssuer = s; + } else if (ref == null && s.startsWith('refs/')) { + ref = s; + } + } + } catch (_) {} + } + } + + // 3. Message signature digest + final msgSig = bundle['messageSignature'] as Map?; + if (msgSig != null && archiveSha256 == null) { + final digest = msgSig['messageDigest']?['digest'] as String?; + if (digest != null) { + try { + final bytes = base64Decode(digest); + archiveSha256 = bytes + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); + } catch (_) {} + } + } + + // 4. DSSE envelope (in-toto statement) + final dsse = bundle['dsseEnvelope'] as Map?; + if (dsse != null) { + final payloadStr = dsse['payload'] as String?; + if (payloadStr != null) { + try { + final payloadBytes = base64Decode(payloadStr); + final statement = + jsonDecode(utf8.decode(payloadBytes)) as Map; + final subjects = statement['subject'] as List?; + if (archiveSha256 == null && + subjects != null && + subjects.isNotEmpty) { + final firstSub = subjects.first as Map; + archiveSha256 = firstSub['digest']?['sha256'] as String?; + } + final predicate = statement['predicate'] as Map?; + final buildDef = + predicate?['buildDefinition'] as Map?; + final extParams = + buildDef?['externalParameters'] as Map?; + final wfObj = extParams?['workflow'] as Map?; + if (wfObj != null) { + repository ??= wfObj['repository'] as String?; + workflow ??= wfObj['path'] as String?; + ref ??= wfObj['ref'] as String?; + } + final runDetails = + predicate?['runDetails'] as Map?; + runUrl ??= runDetails?['metadata']?['invocationId'] as String?; + } catch (_) {} + } + } + + // Fallbacks + repository ??= data.packageLinks.repositoryUrl; + oidcIssuer ??= 'https://token.actions.githubusercontent.com'; + + return AttestationDetails( + slsaLevel: slsaLevel, + repository: repository, + commit: commit, + ref: ref, + workflow: workflow, + runUrl: runUrl, + signerIdentity: signerIdentity, + oidcIssuer: oidcIssuer, + rekorLogIndex: rekorLogIndex, + rekorUrl: rekorUrl, + rekorTimestamp: rekorTimestamp, + archiveSha256: archiveSha256, + ); + } catch (_) { + return AttestationDetails( + slsaLevel: slsaLevel, + repository: data.packageLinks.repositoryUrl, + ); + } + } + + static List _extractAsn1Strings(List bytes) { + final strings = []; + var i = 0; + while (i < bytes.length - 2) { + final tag = bytes[i]; + if (tag == 0x0c || tag == 0x13 || tag == 0x16 || tag == 0x86) { + final lenByte = bytes[i + 1]; + int len; + int offset; + if ((lenByte & 0x80) == 0) { + len = lenByte; + offset = i + 2; + } else { + final numBytes = lenByte & 0x7f; + if (numBytes > 0 && + numBytes <= 4 && + i + 2 + numBytes <= bytes.length) { + len = 0; + for (var j = 0; j < numBytes; j++) { + len = (len << 8) | bytes[i + 2 + j]; + } + offset = i + 2 + numBytes; + } else { + i++; + continue; + } + } + if (len > 0 && offset + len <= bytes.length) { + try { + final str = utf8.decode(bytes.sublist(offset, offset + len)); + if (str.codeUnits.every((c) => c >= 32 && c < 127) && + str.length >= 3) { + strings.add(str); + } + } catch (_) {} + } + } + i++; + } + return strings; + } +} + +/// Renders the Security tab content conforming to OpenSSF Level AAA style guide. +d.Node securityTabNode(PackagePageData data) { + final attestation = AttestationDetails.parse( + data: data, + rawBundleJson: data.asset?.kind == AssetKind.attestation + ? data.asset?.textContent + : null, + ); + + if (attestation != null) { + return _renderAttestedSecurityTab(data, attestation); + } else { + return _renderUnattestedSecurityTab(data); + } +} + +d.Node _renderAttestedSecurityTab( + PackagePageData data, + AttestationDetails attestation, +) { + final package = data.package.name ?? data.version.package; + final version = data.version.version ?? data.version.id!; + final archiveUrl = urls.pkgArchiveDownloadUrl(package, version); + final attestationUrl = urls.pkgAttestationDownloadUrl(package, version); + + return d.div( + classes: ['security-tab'], + children: [ + d.h2(text: 'Build Provenance'), + _renderStatusCard(attestation), + d.div( + classes: ['security-cards-grid'], + children: [ + _renderSourceCard(attestation), + _renderBuildCard(attestation), + _renderAttestationCard(attestation), + _renderIntegrityCard(package, version, attestation, archiveUrl), + ], + ), + d.div( + classes: ['security-download-section'], + children: [ + d.div( + children: [ + d.a( + classes: ['button', 'button-primary'], + href: attestationUrl, + attributes: {'download': '$package-$version.sigstore.json'}, + children: [ + d.unsafeRawHtml( + '', + ), + d.text('Download attestation bundle (.sigstore.json)'), + ], + ), + ], + ), + d.p( + classes: ['-metadata'], + children: [ + d.text('Learn more about '), + d.a( + href: 'https://slsa.dev', + target: '_blank', + rel: 'noopener noreferrer', + text: 'SLSA specifications', + ), + d.text(' and '), + d.a( + href: 'https://dart.dev/tools/pub/automated-publishing', + target: '_blank', + rel: 'noopener noreferrer', + text: 'automated package publishing', + ), + d.text('.'), + ], + ), + ], + ), + ], + ); +} + +d.Node _renderStatusCard(AttestationDetails attestation) { + return d.div( + classes: ['security-status-card'], + children: [ + d.div( + classes: ['security-status-icon'], + child: d.unsafeRawHtml( + '', + ), + ), + d.div( + classes: ['security-status-content'], + children: [ + d.h3( + children: [ + d.text('Build confirmed'), + d.text(' '), + slsaShieldBadgeNode(attestation.slsaLevel), + ], + ), + d.p( + text: + 'This package version was built and published with automated build provenance verified via Sigstore. ' + 'This cryptographically confirms that the package artifact was produced by the source repository\'s automated CI/CD workflow.', + ), + ], + ), + ], + ); +} + +d.Node _renderSourceCard(AttestationDetails attestation) { + final repo = attestation.repository; + final commit = attestation.commit; + final ref = attestation.ref; + + final repoUri = repo != null ? urls.parseValidUrl(repo) : null; + final isGithub = repoUri != null && repoUri.host == 'github.com'; + + final commitUrl = (isGithub && commit != null) + ? '$repo/commit/$commit' + : null; + + return d.div( + classes: ['security-card'], + children: [ + d.div( + classes: ['security-card-header'], + children: [ + d.unsafeRawHtml( + '', + ), + d.h3(text: 'Source'), + ], + ), + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Repository'), + d.div( + classes: ['security-card-value'], + child: repo != null + ? d.a( + href: repo, + text: repo.replaceFirst(RegExp(r'^https?:\/\/'), ''), + target: '_blank', + rel: 'noopener noreferrer ugc', + ) + : d.text('Not specified'), + ), + ], + ), + if (commit != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Commit'), + d.div( + classes: ['security-card-value'], + child: commitUrl != null + ? d.a( + href: commitUrl, + child: d.code(text: commit.substring(0, 7)), + target: '_blank', + rel: 'noopener noreferrer ugc', + attributes: {'title': commit}, + ) + : d.code(text: commit.substring(0, 7)), + ), + ], + ), + if (ref != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Branch / Tag'), + d.div( + classes: ['security-card-value'], + child: d.code(text: ref), + ), + ], + ), + ], + ); +} + +d.Node _renderBuildCard(AttestationDetails attestation) { + final repo = attestation.repository; + final workflow = attestation.workflow; + final runUrl = attestation.runUrl; + + final repoUri = repo != null ? urls.parseValidUrl(repo) : null; + final isGithub = repoUri != null && repoUri.host == 'github.com'; + + final workflowUrl = (isGithub && workflow != null) + ? '$repo/blob/${attestation.commit ?? attestation.ref ?? "HEAD"}/$workflow' + : null; + + return d.div( + classes: ['security-card'], + children: [ + d.div( + classes: ['security-card-header'], + children: [ + d.unsafeRawHtml( + '', + ), + d.h3(text: 'Build'), + ], + ), + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Platform'), + d.div(classes: ['security-card-value'], text: 'GitHub Actions'), + ], + ), + if (workflow != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Workflow'), + d.div( + classes: ['security-card-value'], + child: workflowUrl != null + ? d.a( + href: workflowUrl, + child: d.code(text: workflow), + target: '_blank', + rel: 'noopener noreferrer ugc', + ) + : d.code(text: workflow), + ), + ], + ), + if (runUrl != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Workflow Run'), + d.div( + classes: ['security-card-value'], + child: d.a( + href: runUrl, + text: 'View build execution on GitHub', + target: '_blank', + rel: 'noopener noreferrer ugc', + ), + ), + ], + ), + ], + ); +} + +d.Node _renderAttestationCard(AttestationDetails attestation) { + final identity = attestation.signerIdentity; + final issuer = attestation.oidcIssuer; + final logIndex = attestation.rekorLogIndex; + final rekorUrl = attestation.rekorUrl; + + return d.div( + classes: ['security-card', 'security-card-full-width'], + children: [ + d.div( + classes: ['security-card-header'], + children: [ + d.unsafeRawHtml( + '', + ), + d.h3(text: 'Attestation & Transparency Log'), + ], + ), + if (identity != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div( + classes: ['security-card-label'], + text: 'Signer Workload Identity', + ), + d.div( + classes: ['security-card-value'], + child: d.code(text: identity), + ), + ], + ), + if (issuer != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div( + classes: ['security-card-label'], + text: 'Identity Provider (OIDC)', + ), + d.div( + classes: ['security-card-value'], + child: d.code(text: issuer), + ), + ], + ), + d.div( + classes: ['security-card-row'], + children: [ + d.div( + classes: ['security-card-label'], + text: 'Transparency Log (Rekor)', + ), + d.div( + classes: ['security-card-value'], + child: rekorUrl != null + ? d.fragment([ + d.a( + href: rekorUrl, + text: 'Entry #$logIndex', + target: '_blank', + rel: 'noopener noreferrer', + ), + if (attestation.rekorTimestamp != null) ...[ + d.text(' (recorded '), + d.xAgoTimestamp(attestation.rekorTimestamp!), + d.text(')'), + ], + ]) + : d.text('Recorded in Sigstore public transparency log'), + ), + ], + ), + d.div( + classes: ['security-card-row'], + children: [ + d.div( + classes: ['security-card-label'], + text: 'Certificate Authority', + ), + d.div(classes: ['security-card-value'], text: 'Sigstore (Fulcio)'), + ], + ), + ], + ); +} + +d.Node _renderIntegrityCard( + String package, + String version, + AttestationDetails attestation, + String archiveUrl, +) { + final sha256 = attestation.archiveSha256; + + return d.div( + classes: ['security-card', 'security-card-full-width'], + children: [ + d.div( + classes: ['security-card-header'], + children: [ + d.unsafeRawHtml( + '', + ), + d.h3(text: 'Package Integrity'), + ], + ), + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Archive File'), + d.div( + classes: ['security-card-value'], + child: d.a(href: archiveUrl, text: '$package-$version.tar.gz'), + ), + ], + ), + if (sha256 != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'SHA-256 Checksum'), + d.div( + classes: ['security-card-value'], + child: d.codeSnippet( + language: 'plaintext', + textToCopy: sha256, + text: sha256, + ), + ), + ], + ), + ], + ); +} + +d.Node _renderUnattestedSecurityTab(PackagePageData data) { + final package = data.package.name ?? data.version.package; + final version = data.version.version ?? data.version.id!; + final archiveUrl = urls.pkgArchiveDownloadUrl(package, version); + + return d.div( + classes: ['security-tab'], + children: [ + d.h2(text: 'Build Provenance'), + d.div( + classes: ['markdown-alert', 'markdown-alert-note'], + children: [ + d.p( + classes: ['markdown-alert-title'], + text: 'No build provenance attestation available', + ), + d.p( + text: + 'This version was published without cryptographic build provenance. ' + 'Build provenance provides verifiable proof connecting a package artifact directly to its source repository and automated build workflow.', + ), + d.p( + children: [ + d.text( + 'Package publishers can establish build provenance by publishing through automated CI/CD workflows. ', + ), + d.a( + href: 'https://dart.dev/tools/pub/automated-publishing', + target: '_blank', + rel: 'noopener noreferrer', + text: + 'Learn how to automate publishing with provenance on dart.dev.', + ), + ], + ), + ], + ), + d.div( + classes: ['security-cards-grid'], + children: [ + d.div( + classes: ['security-card', 'security-card-full-width'], + children: [ + d.div( + classes: ['security-card-header'], + children: [ + d.unsafeRawHtml( + '', + ), + d.h3(text: 'Package Archive'), + ], + ), + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Archive File'), + d.div( + classes: ['security-card-value'], + child: d.a( + href: archiveUrl, + text: '$package-$version.tar.gz', + ), + ), + ], + ), + if (data.packageLinks.repositoryUrl != null) + d.div( + classes: ['security-card-row'], + children: [ + d.div(classes: ['security-card-label'], text: 'Repository'), + d.div( + classes: ['security-card-value'], + child: d.a( + href: data.packageLinks.repositoryUrl!, + text: data.packageLinks.repositoryUrl!, + target: '_blank', + rel: 'noopener noreferrer ugc', + ), + ), + ], + ), + ], + ), + ], + ), + ], + ); +} diff --git a/app/lib/frontend/templates/views/pkg/title_content.dart b/app/lib/frontend/templates/views/pkg/title_content.dart index 2ecfd51893..95e4337cc4 100644 --- a/app/lib/frontend/templates/views/pkg/title_content.dart +++ b/app/lib/frontend/templates/views/pkg/title_content.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import '../../../../shared/urls.dart' as urls; import '../../../dom/dom.dart' as d; import '../../../static_files.dart'; import 'badge.dart'; @@ -14,7 +15,13 @@ d.Node titleContentNode({ return d.fragment([ d.text('$package $version '), copyIcon(package: package, version: version), - if (slsaLevel != null) ...[d.text(' '), slsaShieldBadgeNode(slsaLevel)], + if (slsaLevel != null) ...[ + d.text(' '), + slsaShieldBadgeNode( + slsaLevel, + href: urls.pkgSecurityUrl(package, version: version), + ), + ], ]); } diff --git a/app/lib/frontend/templates/views/pkg/versions/version_row.dart b/app/lib/frontend/templates/views/pkg/versions/version_row.dart index 596cfe38c9..f67b44a50b 100644 --- a/app/lib/frontend/templates/views/pkg/versions/version_row.dart +++ b/app/lib/frontend/templates/views/pkg/versions/version_row.dart @@ -32,7 +32,10 @@ d.Node versionRowNode( d.td( classes: ['badge'], child: version.slsaLevel != null - ? slsaShieldBadgeNode(version.slsaLevel!) + ? slsaShieldBadgeNode( + version.slsaLevel!, + href: urls.pkgSecurityUrl(package, version: version.version), + ) : null, ), d.td( diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index acb135c821..bf467be33e 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -2731,6 +2731,7 @@ Future purgePackageCache(String package) async { cache.uiPackageExample(package, null).purge(), cache.uiPackageInstall(package, null).purge(), cache.uiPackageScore(package, null).purge(), + cache.uiPackageSecurity(package, null).purge(), cache.uiPackageVersions(package).purge(), cache.uiLandingPageContent().purge(), cache.allPackagesAtomFeedXml().purge(), diff --git a/app/lib/package/models.dart b/app/lib/package/models.dart index 16f74b6f62..a74a8b8290 100644 --- a/app/lib/package/models.dart +++ b/app/lib/package/models.dart @@ -1253,6 +1253,7 @@ class PackagePageData { bool get hasExample => assets.contains(AssetKind.example); bool get hasLicense => assets.contains(AssetKind.license); bool get hasPubspec => assets.contains(AssetKind.pubspec); + bool get hasAttestation => assets.contains(AssetKind.attestation); bool get isLatestStable => version.version == package.latestVersion; diff --git a/app/lib/shared/redis_cache.dart b/app/lib/shared/redis_cache.dart index 634f7ecaec..268f07163c 100644 --- a/app/lib/shared/redis_cache.dart +++ b/app/lib/shared/redis_cache.dart @@ -104,6 +104,11 @@ class CachePatterns { .withTTL(Duration(minutes: 10)) .withCodec(utf8)['$package-$version']; + Entry uiPackageSecurity(String package, String? version) => _cache + .withPrefix('ui-package-security/') + .withTTL(Duration(minutes: 10)) + .withCodec(utf8)['$package-$version']; + Entry uiPackageVersions(String package) => _cache .withPrefix('ui-package-versions/') .withTTL(Duration(minutes: 10)) diff --git a/app/lib/shared/urls.dart b/app/lib/shared/urls.dart index 477457283e..572662b798 100644 --- a/app/lib/shared/urls.dart +++ b/app/lib/shared/urls.dart @@ -41,6 +41,7 @@ enum PkgPageTab { score, admin, activityLog, + security, } const _pkgPageTabSegments = { @@ -54,6 +55,7 @@ const _pkgPageTabSegments = { PkgPageTab.score: 'score', PkgPageTab.admin: 'admin', PkgPageTab.activityLog: 'activity-log', + PkgPageTab.security: 'security', }; String pkgPageUrl( @@ -115,6 +117,9 @@ String pkgAdminUrl(String package, {bool? includeHost}) => pkgPageUrl( String pkgActivityLogUrl(String package) => pkgPageUrl(package, pkgPageTab: PkgPageTab.activityLog); +String pkgSecurityUrl(String package, {String? version}) => + pkgPageUrl(package, version: version, pkgPageTab: PkgPageTab.security); + String pkgArchiveDownloadUrl(String package, String version, {Uri? baseUri}) { final p = Uri.encodeComponent(package); final v = Uri.encodeComponent(version); @@ -126,6 +131,12 @@ String pkgArchiveDownloadUrl(String package, String version, {Uri? baseUri}) { } } +String pkgAttestationDownloadUrl(String package, String version) { + final p = Uri.encodeComponent(package); + final v = Uri.encodeComponent(version); + return '/api/packages/$p/versions/$v/attestation'; +} + String pkgFeedUrl(String package) { return '/api/packages/$package/feed.atom'; } diff --git a/app/test/frontend/handlers/package_test.dart b/app/test/frontend/handlers/package_test.dart index 356c6ffe85..53d2af38d7 100644 --- a/app/test/frontend/handlers/package_test.dart +++ b/app/test/frontend/handlers/package_test.dart @@ -52,6 +52,14 @@ void main() { await issueGet('/packages/oxygen/versions/1.2.00/changelog'), '/packages/oxygen/versions/1.2.0/changelog', ); + await expectRedirectResponse( + await issueGet('/packages/oxYgen/security'), + '/packages/oxygen/security', + ); + await expectRedirectResponse( + await issueGet('/packages/oxYgen/versions/1.2.0/security'), + '/packages/oxygen/versions/1.2.0/security', + ); }, ); @@ -182,6 +190,7 @@ void main() { '/packages/pkg/pubspec', '/packages/pkg/license', '/packages/pkg/score', + '/packages/pkg/security', ]; for (final url in urls) { await expectHtmlResponse( @@ -193,6 +202,35 @@ void main() { }, ); + testWithProfile( + '/packages/oxygen/security - found', + fn: () async { + await expectHtmlResponse( + await issueGet('/packages/oxygen/security'), + present: [ + 'oxygen 1.2.0', + 'Build Provenance', + 'No build provenance attestation available', + 'Learn how to automate publishing with provenance on dart.dev', + ], + ); + }, + ); + + testWithProfile( + '/packages/oxygen/versions/1.2.0/security - found', + fn: () async { + await expectHtmlResponse( + await issueGet('/packages/oxygen/versions/1.2.0/security'), + present: [ + 'oxygen 1.2.0', + 'Build Provenance', + 'No build provenance attestation available', + ], + ); + }, + ); + testWithProfile( 'publisher redirect', fn: () async { diff --git a/app/test/frontend/security_tab_test.dart b/app/test/frontend/security_tab_test.dart new file mode 100644 index 0000000000..7c14eae8fb --- /dev/null +++ b/app/test/frontend/security_tab_test.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:gcloud/db.dart' as db; +import 'package:pub_dev/frontend/templates/views/pkg/badge.dart'; +import 'package:pub_dev/frontend/templates/views/pkg/security_tab.dart'; +import 'package:pub_dev/package/model_properties.dart'; +import 'package:pub_dev/package/models.dart'; +import 'package:pub_dev/scorecard/models.dart'; +import 'package:pub_dev/shared/urls.dart' as urls; +import 'package:test/test.dart'; + +const _sampleBundleJson = + r'''{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.3", "verificationMaterial": {"certificate": {"rawBytes": "MIIIMTCCB7egAwIBAgIUaL/tsmQTHk21mt1Uuk+w7avDBz4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE5MTcyNjI2WhcNMjQwMzE5MTczNjI2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE22S1j/NkEXzBPQAuamHXLpwx+RPnnzZQl/pkEZ8xorvKnzujCS1mVTBo9kBxmYWo2DHtyVyfgnuOqVTzLYmho6OCBtYwggbSMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUFv1SCziEKN2rRyrjeVlFbSLg1/QwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBChjN2IzZGZiMzM1ZjA1MWUxYzg2YmRhNGM3MTZmYWM5N2RmNjJhZDgxMC0GCisGAQQBg78wAQQEH0V4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDChjN2IzZGZiMzM1ZjA1MWUxYzg2YmRhNGM3MTZmYWM5N2RmNjJhZDgxMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKGM3YjNkZmIzMzVmMDUxZTFjODZiZGE0YzcxNmZhYzk3ZGY2MmFkODEwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYKKwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIEgZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoYzdiM2RmYjMzNWYwNTFlMWM4NmJkYTRjNzE2ZmFjOTdkZjYyYWQ4MTAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMIGBBgorBgEEAYO/MAEVBHMMcWh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi9hY3Rpb25zL3J1bnMvODM0NzQ4MTYyOC9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGOV8AHpgAABAMARzBFAiBFeMbpFarlPwb0naTr4mjWDvXApOd9ORqOk36Brt9SmwIhAJJvjor+DXUXr7S3Vm9jVFT3CL0BxcKGj86m5mYzQvubMAoGCCqGSM49BAMDA2gAMGUCMA8lTixdS4iN9mAUduObcSJmhZLyvK7zaX05DLEDCgPWxDHk+JBZUKYRIuHHgwFnOwIxALMamo9dfENMzRgNCzYfp/y+rSOhVjXXE9mCn6BuJETlpRDfGvxUg/5LF9f4lYqozA=="}, "tlogEntries": [{"logIndex": "79571823", "logId": {"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1710869186", "inclusionPromise": {"signedEntryTimestamp": "MEYCIQDMNM49CNrcrpuvB9G3likdSse0miAkY0ILCqzRGP5ZJQIhAKnSS9GUSFVCar1+Sq3qoRtJIJ8x9tqRnQ8kuS1ojtTH"}, "inclusionProof": {"logIndex": "75408392", "rootHash": "Fnnj13Uu1jdksPc4HZLapKX329dVlD5+MGNsiqBq1XM=", "treeSize": "75408393", "hashes": ["1J7hRIEGvYdAyzEs+GhAE9L+38oHye3BhalgoQRZoo4=", "W/OUCkh/lqDDwbBkZgP7eTV/wx4WifD1wtfRLbavfxI=", "9wya2BEhfLGDfDRVN46OU2RXkozWCM1Z4qMu6SPiWoY=", "ZRs3lKAIlu0t0GtLupAcOu1y20nOaOshSKosWAqFO+w=", "BGqH+LzVuhuqCLiUvBJaB2hlsvtu2a15qq1WGw6mG44=", "OeS7D4kPES7ChE7kWSEmhbAMqBcKVj/z8/afMK4Y3pI=", "JtjqvAqFyXXYjWlZfDzElHpEzdBjsz1LmGFJuYx0kTU=", "s/ZIVcfcD4/nuZwUtQf4ydGsIAkGTPTzk3b0zhUC95k=", "YU1jZY/fp5tJdGF/i+/7ez8107O4/lOUp7acMPFEaOA=", "7Z18YLBAvejEV4nJHIKoks/xlijnhR005qTW2w4QtHg=", "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8="], "checkpoint": {"envelope": "rekor.sigstore.dev - 2605736670972794746\n75408393\nFnnj13Uu1jdksPc4HZLapKX329dVlD5+MGNsiqBq1XM=\n\n— rekor.sigstore.dev wNI9ajBFAiBTyiBM9WtyOTgohje6QZ5rFGJUdMq7Wk3A6oThE98SUgIhAMvxDwa7FyqRqg+YV3rdPPrfS23w19iK+piMSGVOmP5w\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJhMGNmYzcxMjcxZDZlMjc4ZTU3Y2QzMzJmZjk1N2MzZjcwNDNmZGRhMzU0YzRjYmIxOTBhMzBkNTZlZmEwMWJmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQ1lGcS80YlRFZGx1cmdxVnVObXdDY0lXdTNOS09DZ3ZlV0FKQmllekowdUFpRUEyaTdVMTgrYVJwRnhMWWtzcjVIS0JRUXkwOHpFMDUwV0ljMFJ6S3VuRElBPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVbE5WRU5EUWpkbFowRjNTVUpCWjBsVllVd3ZkSE50VVZSSWF6SXhiWFF4VlhWckszYzNZWFpFUW5vMGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UlRWTlZHTjVUbXBKTWxkb1kwNU5hbEYzVFhwRk5VMVVZM3BPYWtreVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVV5TWxNeGFpOU9hMFZZZWtKUVVVRjFZVzFJV0V4d2QzZ3JVbEJ1Ym5wYVVXd3ZjR3NLUlZvNGVHOXlka3R1ZW5WcVExTXhiVlpVUW04NWEwSjRiVmxYYnpKRVNIUjVWbmxtWjI1MVQzRldWSHBNV1cxb2J6WlBRMEowV1hkbloySlRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZHZGpGVENrTjZhVVZMVGpKeVVubHlhbVZXYkVaaVUweG5NUzlSZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJkaFZVZEJNVlZrUlZGRlFpOTNVMEp0YWtOQ2JEUmhRbXhIYURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWHBoVjJSNlpFYzVlUXBhVXpGcVlqSTFiV0l6U25SWlZ6VnFXbE01YkdWSVVubGFWekZzWWtocmRGcEhSblZhTWxaNVlqTldla3hZUWpGWmJYaHdXWGt4ZG1GWFVtcE1WMHBzQ2xsWFRuWmlhVGgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbHNaVWhTZVZwWE1XeGlTR3QwV2tkR2RWb3lWbmxpTTFaNlRGYzVjRnBIVFhRS1dXMVdhRmt5T1hWTWJteDBZa1ZDZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVYSmhTRkl3WTBoTk5ncE1lVGt3WWpKMGJHSnBOV2haTTFKd1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGbVFtZHZja0puUlVWQldVOHZDazFCUlVOQ1FrWXpZak5LY2xwdGVIWmtNVGxyWVZoT2QxbFlVbXBoUkVFeVFtZHZja0puUlVWQldVOHZUVUZGUkVKRGFHcE9Na2w2V2tkYWFVMTZUVEVLV21wQk1VMVhWWGhaZW1jeVdXMVNhRTVIVFROTlZGcHRXVmROTlU0eVVtMU9ha3BvV2tSbmVFMURNRWREYVhOSFFWRlJRbWMzT0hkQlVWRkZTREJXTkFwa1NFcHNZbGRXYzJWVFFtdFpWelZ1V2xoS2RtUllUV2RVTUd4RlVYbENhVnBYUm1waU1qUjNVMUZaUzB0M1dVSkNRVWRFZG5wQlFrSlJVVGRqTW14dUNtTXpVblpqYlZWMFdUSTVkVnB0T1hsaVYwWjFXVEpWZGxwWWFEQmpiVlowV2xkNE5VeFhVbWhpYldSc1kyMDVNV041TVhaaFYxSnFURmRLYkZsWFRuWmlhVFUxWWxkNFFXTnRWbTFqZVRsdkNscFhSbXRqZVRsMFdWZHNkVTFFWjBkRGFYTkhRVkZSUW1jM09IZEJVazFGUzJkM2IxbDZaR2xOTWxKdFdXcE5lazVYV1hkT1ZFWnNUVmROTkU1dFNtc0tXVlJTYWs1NlJUSmFiVVpxVDFSa2ExcHFXWGxaVjFFMFRWUkJhRUpuYjNKQ1owVkZRVmxQTDAxQlJWVkNRazFOUlZoa2RtTnRkRzFpUnpreldESlNjQXBqTTBKb1pFZE9iMDFKUjBKQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtoTlRXTlhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9YcGhWMlI2Q21SSE9YbGFVekZxWWpJMWJXSXpTblJaVnpWcVdsTTViR1ZJVW5sYVZ6RnNZa2hyZEZwSFJuVmFNbFo1WWpOV2VreFlRakZaYlhod1dYa3hkbUZYVW1vS1RGZEtiRmxYVG5aaWFUbG9XVE5TY0dJeU5YcE1NMG94WW01TmRrOUVUVEJPZWxFMFRWUlplVTlET1doa1NGSnNZbGhDTUdONU9IaE5RbGxIUTJselJ3cEJVVkZDWnpjNGQwRlNXVVZEUVhkSFkwaFdhV0pIYkdwTlNVZExRbWR2Y2tKblJVVkJaRm8xUVdkUlEwSklkMFZsWjBJMFFVaFpRVE5VTUhkaGMySklDa1ZVU21wSFVqUmpiVmRqTTBGeFNrdFljbXBsVUVzekwyZzBjSGxuUXpod04yODBRVUZCUjA5V09FRkljR2RCUVVKQlRVRlNla0pHUVdsQ1JtVk5ZbkFLUm1GeWJGQjNZakJ1WVZSeU5HMXFWMFIyV0VGd1QyUTVUMUp4VDJzek5rSnlkRGxUYlhkSmFFRktTblpxYjNJclJGaFZXSEkzVXpOV2JUbHFWa1pVTXdwRFREQkNlR05MUjJvNE5tMDFiVmw2VVhaMVlrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tZEJUVWRWUTAxQk9HeFVhWGhrVXpScFRqbHRRVlZrZFU5aUNtTlRTbTFvV2t4NWRrczNlbUZZTURWRVRFVkVRMmRRVjNoRVNHc3JTa0phVlV0WlVrbDFTRWhuZDBadVQzZEplRUZNVFdGdGJ6bGtaa1ZPVFhwU1owNEtRM3BaWm5BdmVTdHlVMDlvVm1wWVdFVTViVU51TmtKMVNrVlViSEJTUkdaSGRuaFZaeTgxVEVZNVpqUnNXWEZ2ZWtFOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifX19fQ=="}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "oM/HEnHW4njlfNMy/5V8P3BD/do1TEy7GQow1W76Ab8="}, "signature": "MEUCICYFq/4bTEdlurgqVuNmwCcIWu3NKOCgveWAJBiezJ0uAiEA2i7U18+aRpFxLYksr5HKBQQy08zE050WIc0RzKunDIA="}}'''; + +PackagePageData _createMockData({int? slsaLevel, String? attestationJson}) { + final pubspec = Pubspec.fromYaml(''' +name: test_pkg +version: 1.0.0 +description: A package for testing. +repository: https://github.com/example/test_pkg +'''); + + final partition = db.Partition(null); + final rootKey = db.Key.emptyKey(partition); + final packageKey = rootKey.append(Package, id: 'test_pkg'); + + final version = PackageVersion.init() + ..id = '1.0.0' + ..version = '1.0.0' + ..parentKey = packageKey + ..packageKey = packageKey + ..created = DateTime.utc(2026, 1, 1) + ..pubspec = pubspec + ..uploader = 'test@example.com' + ..slsaLevel = slsaLevel; + + final package = Package.fromVersion(version); + + final versionInfo = PackageVersionInfo() + ..initFromKey(QualifiedVersionKey(package: 'test_pkg', version: '1.0.0')) + ..versionCreated = DateTime.utc(2026, 1, 1) + ..assets = [ + AssetKind.pubspec, + AssetKind.readme, + if (attestationJson != null) AssetKind.attestation, + ]; + + final asset = attestationJson != null + ? (PackageVersionAsset.init( + package: 'test_pkg', + version: '1.0.0', + kind: AssetKind.attestation, + versionCreated: DateTime.utc(2026, 1, 1), + path: 'test_pkg-1.0.0.sigstore.json', + textContent: attestationJson, + )) + : null; + + return PackagePageData( + package: package, + version: version, + versionInfo: versionInfo, + asset: asset, + scoreCard: ScoreCardData(), + isAdmin: false, + isLiked: false, + weeklyDownloadCounts: null, + ); +} + +void main() { + group('AttestationDetails parsing', () { + test('parses Sigstore bundle with certificate and Rekor log', () { + final data = _createMockData( + slsaLevel: 2, + attestationJson: _sampleBundleJson, + ); + final details = AttestationDetails.parse( + data: data, + rawBundleJson: _sampleBundleJson, + ); + + expect(details, isNotNull); + expect(details!.slsaLevel, 2); + expect(details.rekorLogIndex, '79571823'); + expect( + details.rekorUrl, + 'https://search.sigstore.dev/?logIndex=79571823', + ); + expect(details.rekorTimestamp, isNotNull); + expect( + details.archiveSha256, + 'a0cfc71271d6e278e57cd332ff957c3f7043fdda354c4cbb190a30d56efa01bf', + ); + expect( + details.repository, + 'https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon', + ); + expect( + details.workflow, + '.github/workflows/extremely-dangerous-oidc-beacon.yml', + ); + expect(details.ref, 'refs/heads/main'); + expect(details.commit, 'c7b3dfb335f051e1c86bda4c716fac97df62ad81'); + expect( + details.runUrl, + 'https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/actions/runs/8347481628/attempts/1', + ); + expect( + details.signerIdentity, + 'https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main', + ); + expect(details.oidcIssuer, 'https://token.actions.githubusercontent.com'); + }); + + test('returns null when no attestation and slsaLevel is null', () { + final data = _createMockData(slsaLevel: null); + final details = AttestationDetails.parse(data: data, rawBundleJson: null); + expect(details, isNull); + }); + }); + + group('securityTabNode UI rendering', () { + test('renders full Level AAA provenance details when attestation exists', () { + final data = _createMockData( + slsaLevel: 2, + attestationJson: _sampleBundleJson, + ); + final node = securityTabNode(data); + final html = node.toString(); + + // Heading and summary + expect(html, contains('

Build Provenance

')); + expect(html, contains('Build confirmed')); + expect(html, contains('SLSA 2')); + expect(html, contains('package-badge-slsa')); + + // Source card + expect(html, contains('Source')); + expect( + html, + contains( + 'github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon', + ), + ); + expect(html, contains('c7b3dfb')); + expect(html, contains('refs/heads/main')); + + // Build card + expect(html, contains('Build')); + expect(html, contains('GitHub Actions')); + expect( + html, + contains( + '.github/workflows/extremely-dangerous-oidc-beacon.yml', + ), + ); + expect(html, contains('actions/runs/8347481628')); + + // Attestation card + expect(html, contains('Attestation & Transparency Log')); + expect(html, contains('token.actions.githubusercontent.com')); + expect(html, contains('https://search.sigstore.dev/?logIndex=79571823')); + expect(html, contains('Sigstore (Fulcio)')); + + // Integrity card + expect(html, contains('Package Integrity')); + expect(html, contains('test_pkg-1.0.0.tar.gz')); + expect( + html, + contains( + 'a0cfc71271d6e278e57cd332ff957c3f7043fdda354c4cbb190a30d56efa01bf', + ), + ); + + // Download section + expect(html, contains('Download attestation bundle (.sigstore.json)')); + expect( + html, + contains('/api/packages/test_pkg/versions/1.0.0/attestation'), + ); + }); + + test('renders educational empty state when no attestation exists', () { + final data = _createMockData(slsaLevel: null); + final node = securityTabNode(data); + final html = node.toString(); + + expect(html, contains('

Build Provenance

')); + expect(html, contains('No build provenance attestation available')); + expect(html, contains('https://dart.dev/tools/pub/automated-publishing')); + expect(html, contains('test_pkg-1.0.0.tar.gz')); + }); + }); + + group('slsaShieldBadgeNode', () { + test('renders span when href is null', () { + final node = slsaShieldBadgeNode(2); + final html = node.toString(); + expect( + html, + startsWith(' Date: Wed, 2 Sep 2026 14:06:34 +0000 Subject: [PATCH 3/6] Align Security tab and info box with OpenSSF Level AAA recommendations - Add explicit provenance vs code safety disclaimer in the status card per OpenSSF Principle 4 & 7. - Add full URL disclosure hover tooltips for workflow run and Rekor log links. - Add minimal build provenance bookmark block to the package info box linking to the Security tab. --- .../templates/views/pkg/info_box.dart | 22 ++++++++ .../templates/views/pkg/security_tab.dart | 5 +- app/test/frontend/security_tab_test.dart | 53 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/app/lib/frontend/templates/views/pkg/info_box.dart b/app/lib/frontend/templates/views/pkg/info_box.dart index 8662e6a6c5..ccf6dc0e94 100644 --- a/app/lib/frontend/templates/views/pkg/info_box.dart +++ b/app/lib/frontend/templates/views/pkg/info_box.dart @@ -15,6 +15,7 @@ import '../../../../shared/urls.dart' as urls; import '../../../../task/backend.dart'; import '../../../dom/dom.dart' as d; import '../shared/images.dart'; +import 'badge.dart'; import 'screenshots.dart'; /// Links inside the package info box. @@ -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!, @@ -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), diff --git a/app/lib/frontend/templates/views/pkg/security_tab.dart b/app/lib/frontend/templates/views/pkg/security_tab.dart index 54d17f0ace..95f93cb0ec 100644 --- a/app/lib/frontend/templates/views/pkg/security_tab.dart +++ b/app/lib/frontend/templates/views/pkg/security_tab.dart @@ -371,7 +371,8 @@ d.Node _renderStatusCard(AttestationDetails attestation) { d.p( text: 'This package version was built and published with automated build provenance verified via Sigstore. ' - 'This cryptographically confirms that the package artifact was produced by the source repository\'s automated CI/CD workflow.', + 'This cryptographically confirms that the package artifact was produced by the source repository\'s automated CI/CD workflow. ' + 'Provenance verifies where and how this package was built, but does not guarantee that the code is free of bugs, security vulnerabilities, or malicious behavior.', ), ], ), @@ -515,6 +516,7 @@ d.Node _renderBuildCard(AttestationDetails attestation) { text: 'View build execution on GitHub', target: '_blank', rel: 'noopener noreferrer ugc', + attributes: {'title': runUrl}, ), ), ], @@ -585,6 +587,7 @@ d.Node _renderAttestationCard(AttestationDetails attestation) { text: 'Entry #$logIndex', target: '_blank', rel: 'noopener noreferrer', + attributes: {'title': rekorUrl}, ), if (attestation.rekorTimestamp != null) ...[ d.text(' (recorded '), diff --git a/app/test/frontend/security_tab_test.dart b/app/test/frontend/security_tab_test.dart index 7c14eae8fb..3b5d34b7db 100644 --- a/app/test/frontend/security_tab_test.dart +++ b/app/test/frontend/security_tab_test.dart @@ -3,7 +3,9 @@ // BSD-style license that can be found in the LICENSE file. import 'package:gcloud/db.dart' as db; +import 'package:pub_dev/frontend/dom/dom.dart' as d; import 'package:pub_dev/frontend/templates/views/pkg/badge.dart'; +import 'package:pub_dev/frontend/templates/views/pkg/info_box.dart'; import 'package:pub_dev/frontend/templates/views/pkg/security_tab.dart'; import 'package:pub_dev/package/model_properties.dart'; import 'package:pub_dev/package/models.dart'; @@ -181,6 +183,26 @@ void main() { html, contains('/api/packages/test_pkg/versions/1.0.0/attestation'), ); + + // Disclaimer and accessibility tooltips + expect( + html, + contains( + 'does not guarantee that the code is free of bugs, security vulnerabilities, or malicious behavior', + ), + ); + expect( + html, + contains( + 'title="https://search.sigstore.dev/?logIndex=79571823"', + ), + ); + expect( + html, + contains( + 'title="https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/actions/runs/8347481628/attempts/1"', + ), + ); }); test('renders educational empty state when no attestation exists', () { @@ -221,4 +243,35 @@ void main() { expect(html, contains('SLSA 2')); }); }); + + group('packageInfoBoxNode', () { + test('renders provenance link when slsaLevel is present', () { + final data = _createMockData(slsaLevel: 2); + final node = packageInfoBoxNode( + data: data, + metaLinks: [], + docLinks: [], + fundingLinks: [], + labeledScores: d.div(), + ); + final html = node.toString(); + expect(html, contains('Provenance')); + expect(html, contains('SLSA 2')); + expect(html, contains('href="/packages/test_pkg/security"')); + expect(html, contains('View details')); + }); + + test('does not render provenance link when slsaLevel is null', () { + final data = _createMockData(slsaLevel: null); + final node = packageInfoBoxNode( + data: data, + metaLinks: [], + docLinks: [], + fundingLinks: [], + labeledScores: d.div(), + ); + final html = node.toString(); + expect(html.contains('Provenance'), isFalse); + }); + }); } From 4b2099faec0af45edd7c8cbfe2a826fa80bc67aa Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 12 Oct 2026 08:45:12 +0000 Subject: [PATCH 4/6] Adapt Security tab to main: PackagePageData.assets and asset redirect `PackagePageData` now carries `assets` directly instead of a `PackageVersionInfo`, and `_handlePackagePage` redirects tab pages whose asset is missing to the readme. The security page is rendered for every version, with or without an attestation, so it is exempt from that redirect. --- app/lib/frontend/handlers/package.dart | 3 +++ app/test/frontend/security_tab_test.dart | 19 +++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/lib/frontend/handlers/package.dart b/app/lib/frontend/handlers/package.dart index ac54b98bbf..52d206d82b 100644 --- a/app/lib/frontend/handlers/package.dart +++ b/app/lib/frontend/handlers/package.dart @@ -376,8 +376,11 @@ Future _handlePackagePage({ } // If the asset does not exists (e.g. package without a changelog or example), redirect to the readme page. + // The security page is rendered for every version, with or without an + // attestation. if (assetKind != null && assetKind != AssetKind.readme && + assetKind != AssetKind.attestation && data.asset == null) { return redirectResponse(urls.pkgPageUrl(packageName, version: versionName)); } diff --git a/app/test/frontend/security_tab_test.dart b/app/test/frontend/security_tab_test.dart index 3b5d34b7db..6f54e35030 100644 --- a/app/test/frontend/security_tab_test.dart +++ b/app/test/frontend/security_tab_test.dart @@ -40,14 +40,11 @@ repository: https://github.com/example/test_pkg final package = Package.fromVersion(version); - final versionInfo = PackageVersionInfo() - ..initFromKey(QualifiedVersionKey(package: 'test_pkg', version: '1.0.0')) - ..versionCreated = DateTime.utc(2026, 1, 1) - ..assets = [ - AssetKind.pubspec, - AssetKind.readme, - if (attestationJson != null) AssetKind.attestation, - ]; + final assets = [ + AssetKind.pubspec, + AssetKind.readme, + if (attestationJson != null) AssetKind.attestation, + ]; final asset = attestationJson != null ? (PackageVersionAsset.init( @@ -63,7 +60,7 @@ repository: https://github.com/example/test_pkg return PackagePageData( package: package, version: version, - versionInfo: versionInfo, + assets: assets, asset: asset, scoreCard: ScoreCardData(), isAdmin: false, @@ -193,9 +190,7 @@ void main() { ); expect( html, - contains( - 'title="https://search.sigstore.dev/?logIndex=79571823"', - ), + contains('title="https://search.sigstore.dev/?logIndex=79571823"'), ); expect( html, From 8714e08f37a27726d71d28b1d865818eeb1c5130 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 12 Oct 2026 09:20:34 +0000 Subject: [PATCH 5/6] Fix tests for the security page: route count and SCSS color variable - `invalid_package_url_test` counts the two new `/security` routes. - The security status icon background is defined in `_variables.scss` (light and dark theme) instead of inline in `_pkg.scss`, as required by `color_expressions_test`. --- app/test/frontend/handlers/invalid_package_url_test.dart | 2 +- pkg/web_css/lib/src/_pkg.scss | 2 +- pkg/web_css/lib/src/_variables.scss | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/test/frontend/handlers/invalid_package_url_test.dart b/app/test/frontend/handlers/invalid_package_url_test.dart index 9c49005ac5..518639ce78 100644 --- a/app/test/frontend/handlers/invalid_package_url_test.dart +++ b/app/test/frontend/handlers/invalid_package_url_test.dart @@ -34,7 +34,7 @@ void main() { expect(urls, contains('/packages//versions/')); expect(urls, contains('/api/packages/')); // this a naive assertion that fails, if new end-points are introduced! - expect(urls, hasLength(45), reason: 'check if new end-points was added'); + expect(urls, hasLength(47), reason: 'check if new end-points was added'); }); testWithProfile( diff --git a/pkg/web_css/lib/src/_pkg.scss b/pkg/web_css/lib/src/_pkg.scss index 3e7b67fb19..11c300d412 100644 --- a/pkg/web_css/lib/src/_pkg.scss +++ b/pkg/web_css/lib/src/_pkg.scss @@ -633,7 +633,7 @@ align-items: center; justify-content: center; border-radius: 50%; - background: #e8f5e9; + background: var(--pub-security-status-icon-bgColor); svg { display: block; diff --git a/pkg/web_css/lib/src/_variables.scss b/pkg/web_css/lib/src/_variables.scss index f9d2816c6f..3460fc1ba3 100644 --- a/pkg/web_css/lib/src/_variables.scss +++ b/pkg/web_css/lib/src/_variables.scss @@ -135,6 +135,7 @@ --pub-badge-red-color: var(--pub-color-dangerRed); --pub-button-danger-background: var(--pub-color-dangerRed); --pub-copy_feedback-background-color: #fafaff; + --pub-security-status-icon-bgColor: #e8f5e9; --pub-detail_tab-text-color: var(--pub-neutral-textColor); --pub-detail_tab-underline-color: #dddddd; --pub-detail_tab-active-color: var(--pub-link-text-color); @@ -194,6 +195,7 @@ --pub-badge-red-color: var(--pub-color-dangerRed); --pub-button-danger-background: #a03737; --pub-copy_feedback-background-color: #404040; + --pub-security-status-icon-bgColor: #1e3a23; --pub-detail_tab-text-color: var(--pub-neutral-textColor); --pub-detail_tab-underline-color: #888888; --pub-detail_tab-active-color: var(--pub-link-text-color); From b7c1e390a4489a597959fe69fff7157385d9694d Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 12 Oct 2026 13:01:29 +0000 Subject: [PATCH 6/6] Render the Security tab from the persisted provenance facts The attestation is no longer a PackageVersionAsset (it lives in the canonical bucket, see #9545) and the facts pub.dev verified at upload are persisted on the PackageVersion (#9546). The Security tab is now rendered from those, instead of re-deriving them from the bundle (including the ad-hoc ASN.1 string scan of the certificate), and the security page no longer loads an asset. --- app/lib/frontend/handlers/package.dart | 7 +- .../templates/views/pkg/security_tab.dart | 259 +++--------------- app/lib/package/models.dart | 4 +- app/test/frontend/security_tab_test.dart | 96 ++++--- 4 files changed, 97 insertions(+), 269 deletions(-) diff --git a/app/lib/frontend/handlers/package.dart b/app/lib/frontend/handlers/package.dart index 52d206d82b..34dded9d10 100644 --- a/app/lib/frontend/handlers/package.dart +++ b/app/lib/frontend/handlers/package.dart @@ -250,7 +250,9 @@ Future packageSecurityHandler( request: request, packageName: packageName, versionName: versionName, - assetKind: AssetKind.attestation, + // The page is rendered from the provenance facts persisted with the + // version, it does not need an asset. + assetKind: null, requiresAdmin: false, canonicalUrlFn: (p, v) => urls.pkgSecurityUrl(p, version: v), renderFn: (data) => renderPkgSecurityPage(data), @@ -376,11 +378,8 @@ Future _handlePackagePage({ } // If the asset does not exists (e.g. package without a changelog or example), redirect to the readme page. - // The security page is rendered for every version, with or without an - // attestation. if (assetKind != null && assetKind != AssetKind.readme && - assetKind != AssetKind.attestation && data.asset == null) { return redirectResponse(urls.pkgPageUrl(packageName, version: versionName)); } diff --git a/app/lib/frontend/templates/views/pkg/security_tab.dart b/app/lib/frontend/templates/views/pkg/security_tab.dart index 95f93cb0ec..cee6476f08 100644 --- a/app/lib/frontend/templates/views/pkg/security_tab.dart +++ b/app/lib/frontend/templates/views/pkg/security_tab.dart @@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:convert'; - import '../../../../package/models.dart'; import '../../../../shared/urls.dart' as urls; import '../../../dom/dom.dart' as d; @@ -39,240 +37,55 @@ class AttestationDetails { this.archiveSha256, }); - static AttestationDetails? parse({ - required PackagePageData data, - String? rawBundleJson, - }) { - final slsaLevel = data.version.slsaLevel ?? 2; - if (rawBundleJson == null || rawBundleJson.trim().isEmpty) { - if (data.version.slsaLevel != null) { - return AttestationDetails( - slsaLevel: slsaLevel, - repository: data.packageLinks.repositoryUrl, - ); - } - return null; - } - - try { - final bundle = jsonDecode(rawBundleJson) as Map; - - String? rekorLogIndex; - String? rekorUrl; - DateTime? rekorTimestamp; - String? archiveSha256; - String? repository; - String? commit; - String? ref; - String? workflow; - String? runUrl; - String? signerIdentity; - String? oidcIssuer; - - // 1. Transparency log (Rekor) entries - final vm = bundle['verificationMaterial'] as Map?; - if (vm != null) { - final tlogEntries = vm['tlogEntries'] as List?; - if (tlogEntries != null && tlogEntries.isNotEmpty) { - final first = tlogEntries.first as Map; - final indexVal = first['logIndex']; - if (indexVal != null) { - rekorLogIndex = indexVal.toString(); - rekorUrl = 'https://search.sigstore.dev/?logIndex=$rekorLogIndex'; - } - final timeVal = first['integratedTime']; - if (timeVal != null) { - final seconds = int.tryParse(timeVal.toString()); - if (seconds != null) { - rekorTimestamp = DateTime.fromMillisecondsSinceEpoch( - seconds * 1000, - isUtc: true, - ); - } - } - if (first['canonicalizedBody'] != null) { - try { - final bodyBytes = base64Decode( - first['canonicalizedBody'] as String, - ); - final bodyJson = - jsonDecode(utf8.decode(bodyBytes)) as Map; - final spec = bodyJson['spec'] as Map?; - final hashVal = spec?['data']?['hash']?['value'] as String?; - if (hashVal != null && hashVal.isNotEmpty) { - archiveSha256 = hashVal; - } - } catch (_) {} - } - } - - // 2. Certificate parsing (ASN.1 DER strings) - final cert = vm['certificate'] as Map?; - if (cert != null && cert['rawBytes'] is String) { - try { - final certBytes = base64Decode(cert['rawBytes'] as String); - final strings = _extractAsn1Strings(certBytes); - for (final s in strings) { - if (s.contains('github.com/') && - s.contains('/.github/workflows/')) { - signerIdentity ??= s; - final atIdx = s.lastIndexOf('@'); - final baseUri = atIdx != -1 ? s.substring(0, atIdx) : s; - if (atIdx != -1 && ref == null) { - final refPart = s.substring(atIdx + 1); - if (refPart.startsWith('refs/')) { - ref = refPart; - } - } - final wfIdx = baseUri.indexOf('/.github/workflows/'); - if (wfIdx != -1) { - repository ??= baseUri.substring(0, wfIdx); - workflow ??= baseUri.substring(wfIdx + 1); - } - } else if (commit == null && - s.length == 40 && - RegExp(r'^[0-9a-fA-F]{40}$').hasMatch(s)) { - commit = s; - } else if (runUrl == null && - s.contains('github.com/') && - s.contains('/actions/runs/')) { - runUrl = s; - } else if (oidcIssuer == null && - s.contains('token.actions.githubusercontent.com')) { - oidcIssuer = s; - } else if (ref == null && s.startsWith('refs/')) { - ref = s; - } - } - } catch (_) {} - } + /// The details of the attestation of the version in [data], from the + /// provenance facts pub.dev verified and persisted at upload - the bundle + /// itself is not parsed here. + /// + /// Returns `null` if the version was published without an attestation. + static AttestationDetails? fromPageData(PackagePageData data) { + final version = data.version; + final provenance = version.provenance; + final slsaLevel = version.slsaLevel; + if (provenance == null) { + if (slsaLevel == null) { + return null; } - - // 3. Message signature digest - final msgSig = bundle['messageSignature'] as Map?; - if (msgSig != null && archiveSha256 == null) { - final digest = msgSig['messageDigest']?['digest'] as String?; - if (digest != null) { - try { - final bytes = base64Decode(digest); - archiveSha256 = bytes - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); - } catch (_) {} - } - } - - // 4. DSSE envelope (in-toto statement) - final dsse = bundle['dsseEnvelope'] as Map?; - if (dsse != null) { - final payloadStr = dsse['payload'] as String?; - if (payloadStr != null) { - try { - final payloadBytes = base64Decode(payloadStr); - final statement = - jsonDecode(utf8.decode(payloadBytes)) as Map; - final subjects = statement['subject'] as List?; - if (archiveSha256 == null && - subjects != null && - subjects.isNotEmpty) { - final firstSub = subjects.first as Map; - archiveSha256 = firstSub['digest']?['sha256'] as String?; - } - final predicate = statement['predicate'] as Map?; - final buildDef = - predicate?['buildDefinition'] as Map?; - final extParams = - buildDef?['externalParameters'] as Map?; - final wfObj = extParams?['workflow'] as Map?; - if (wfObj != null) { - repository ??= wfObj['repository'] as String?; - workflow ??= wfObj['path'] as String?; - ref ??= wfObj['ref'] as String?; - } - final runDetails = - predicate?['runDetails'] as Map?; - runUrl ??= runDetails?['metadata']?['invocationId'] as String?; - } catch (_) {} - } - } - - // Fallbacks - repository ??= data.packageLinks.repositoryUrl; - oidcIssuer ??= 'https://token.actions.githubusercontent.com'; - - return AttestationDetails( - slsaLevel: slsaLevel, - repository: repository, - commit: commit, - ref: ref, - workflow: workflow, - runUrl: runUrl, - signerIdentity: signerIdentity, - oidcIssuer: oidcIssuer, - rekorLogIndex: rekorLogIndex, - rekorUrl: rekorUrl, - rekorTimestamp: rekorTimestamp, - archiveSha256: archiveSha256, - ); - } catch (_) { + // Attested, but the facts are not available (e.g. a version from + // before they were persisted): show what we know. return AttestationDetails( slsaLevel: slsaLevel, repository: data.packageLinks.repositoryUrl, + archiveSha256: _hex(version.sha256), ); } + final logIndex = provenance.transparencyLogIndex; + return AttestationDetails( + slsaLevel: slsaLevel ?? 2, + repository: provenance.repositoryUrl, + commit: provenance.commitSha, + ref: provenance.ref, + workflow: provenance.workflowPath, + runUrl: provenance.runUrl, + signerIdentity: provenance.signer, + oidcIssuer: provenance.issuer, + rekorLogIndex: logIndex?.toString(), + rekorUrl: logIndex == null + ? null + : 'https://search.sigstore.dev/?logIndex=$logIndex', + rekorTimestamp: provenance.transparencyLogIntegratedTime, + archiveSha256: _hex(version.sha256), + ); } - static List _extractAsn1Strings(List bytes) { - final strings = []; - var i = 0; - while (i < bytes.length - 2) { - final tag = bytes[i]; - if (tag == 0x0c || tag == 0x13 || tag == 0x16 || tag == 0x86) { - final lenByte = bytes[i + 1]; - int len; - int offset; - if ((lenByte & 0x80) == 0) { - len = lenByte; - offset = i + 2; - } else { - final numBytes = lenByte & 0x7f; - if (numBytes > 0 && - numBytes <= 4 && - i + 2 + numBytes <= bytes.length) { - len = 0; - for (var j = 0; j < numBytes; j++) { - len = (len << 8) | bytes[i + 2 + j]; - } - offset = i + 2 + numBytes; - } else { - i++; - continue; - } - } - if (len > 0 && offset + len <= bytes.length) { - try { - final str = utf8.decode(bytes.sublist(offset, offset + len)); - if (str.codeUnits.every((c) => c >= 32 && c < 127) && - str.length >= 3) { - strings.add(str); - } - } catch (_) {} - } - } - i++; - } - return strings; + static String? _hex(List? bytes) { + if (bytes == null || bytes.isEmpty) return null; + return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } } /// Renders the Security tab content conforming to OpenSSF Level AAA style guide. d.Node securityTabNode(PackagePageData data) { - final attestation = AttestationDetails.parse( - data: data, - rawBundleJson: data.asset?.kind == AssetKind.attestation - ? data.asset?.textContent - : null, - ); + final attestation = AttestationDetails.fromPageData(data); if (attestation != null) { return _renderAttestedSecurityTab(data, attestation); diff --git a/app/lib/package/models.dart b/app/lib/package/models.dart index a74a8b8290..b9603248e6 100644 --- a/app/lib/package/models.dart +++ b/app/lib/package/models.dart @@ -1253,7 +1253,9 @@ class PackagePageData { bool get hasExample => assets.contains(AssetKind.example); bool get hasLicense => assets.contains(AssetKind.license); bool get hasPubspec => assets.contains(AssetKind.pubspec); - bool get hasAttestation => assets.contains(AssetKind.attestation); + + /// Whether the version was published with a (verified) attestation. + bool get hasAttestation => version.provenance != null; bool get isLatestStable => version.version == package.latestVersion; diff --git a/app/test/frontend/security_tab_test.dart b/app/test/frontend/security_tab_test.dart index 6f54e35030..de5fa30b0e 100644 --- a/app/test/frontend/security_tab_test.dart +++ b/app/test/frontend/security_tab_test.dart @@ -13,10 +13,36 @@ import 'package:pub_dev/scorecard/models.dart'; import 'package:pub_dev/shared/urls.dart' as urls; import 'package:test/test.dart'; -const _sampleBundleJson = - r'''{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.3", "verificationMaterial": {"certificate": {"rawBytes": "MIIIMTCCB7egAwIBAgIUaL/tsmQTHk21mt1Uuk+w7avDBz4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE5MTcyNjI2WhcNMjQwMzE5MTczNjI2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE22S1j/NkEXzBPQAuamHXLpwx+RPnnzZQl/pkEZ8xorvKnzujCS1mVTBo9kBxmYWo2DHtyVyfgnuOqVTzLYmho6OCBtYwggbSMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUFv1SCziEKN2rRyrjeVlFbSLg1/QwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBChjN2IzZGZiMzM1ZjA1MWUxYzg2YmRhNGM3MTZmYWM5N2RmNjJhZDgxMC0GCisGAQQBg78wAQQEH0V4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDChjN2IzZGZiMzM1ZjA1MWUxYzg2YmRhNGM3MTZmYWM5N2RmNjJhZDgxMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKGM3YjNkZmIzMzVmMDUxZTFjODZiZGE0YzcxNmZhYzk3ZGY2MmFkODEwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYKKwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIEgZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoYzdiM2RmYjMzNWYwNTFlMWM4NmJkYTRjNzE2ZmFjOTdkZjYyYWQ4MTAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMIGBBgorBgEEAYO/MAEVBHMMcWh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi9hY3Rpb25zL3J1bnMvODM0NzQ4MTYyOC9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGOV8AHpgAABAMARzBFAiBFeMbpFarlPwb0naTr4mjWDvXApOd9ORqOk36Brt9SmwIhAJJvjor+DXUXr7S3Vm9jVFT3CL0BxcKGj86m5mYzQvubMAoGCCqGSM49BAMDA2gAMGUCMA8lTixdS4iN9mAUduObcSJmhZLyvK7zaX05DLEDCgPWxDHk+JBZUKYRIuHHgwFnOwIxALMamo9dfENMzRgNCzYfp/y+rSOhVjXXE9mCn6BuJETlpRDfGvxUg/5LF9f4lYqozA=="}, "tlogEntries": [{"logIndex": "79571823", "logId": {"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion": {"kind": "hashedrekord", "version": "0.0.1"}, "integratedTime": "1710869186", "inclusionPromise": {"signedEntryTimestamp": "MEYCIQDMNM49CNrcrpuvB9G3likdSse0miAkY0ILCqzRGP5ZJQIhAKnSS9GUSFVCar1+Sq3qoRtJIJ8x9tqRnQ8kuS1ojtTH"}, "inclusionProof": {"logIndex": "75408392", "rootHash": "Fnnj13Uu1jdksPc4HZLapKX329dVlD5+MGNsiqBq1XM=", "treeSize": "75408393", "hashes": ["1J7hRIEGvYdAyzEs+GhAE9L+38oHye3BhalgoQRZoo4=", "W/OUCkh/lqDDwbBkZgP7eTV/wx4WifD1wtfRLbavfxI=", "9wya2BEhfLGDfDRVN46OU2RXkozWCM1Z4qMu6SPiWoY=", "ZRs3lKAIlu0t0GtLupAcOu1y20nOaOshSKosWAqFO+w=", "BGqH+LzVuhuqCLiUvBJaB2hlsvtu2a15qq1WGw6mG44=", "OeS7D4kPES7ChE7kWSEmhbAMqBcKVj/z8/afMK4Y3pI=", "JtjqvAqFyXXYjWlZfDzElHpEzdBjsz1LmGFJuYx0kTU=", "s/ZIVcfcD4/nuZwUtQf4ydGsIAkGTPTzk3b0zhUC95k=", "YU1jZY/fp5tJdGF/i+/7ez8107O4/lOUp7acMPFEaOA=", "7Z18YLBAvejEV4nJHIKoks/xlijnhR005qTW2w4QtHg=", "98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8="], "checkpoint": {"envelope": "rekor.sigstore.dev - 2605736670972794746\n75408393\nFnnj13Uu1jdksPc4HZLapKX329dVlD5+MGNsiqBq1XM=\n\n— rekor.sigstore.dev wNI9ajBFAiBTyiBM9WtyOTgohje6QZ5rFGJUdMq7Wk3A6oThE98SUgIhAMvxDwa7FyqRqg+YV3rdPPrfS23w19iK+piMSGVOmP5w\n"}}, "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJhMGNmYzcxMjcxZDZlMjc4ZTU3Y2QzMzJmZjk1N2MzZjcwNDNmZGRhMzU0YzRjYmIxOTBhMzBkNTZlZmEwMWJmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQ1lGcS80YlRFZGx1cmdxVnVObXdDY0lXdTNOS09DZ3ZlV0FKQmllekowdUFpRUEyaTdVMTgrYVJwRnhMWWtzcjVIS0JRUXkwOHpFMDUwV0ljMFJ6S3VuRElBPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVbE5WRU5EUWpkbFowRjNTVUpCWjBsVllVd3ZkSE50VVZSSWF6SXhiWFF4VlhWckszYzNZWFpFUW5vMGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDE2UlRWTlZHTjVUbXBKTWxkb1kwNU5hbEYzVFhwRk5VMVVZM3BPYWtreVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVV5TWxNeGFpOU9hMFZZZWtKUVVVRjFZVzFJV0V4d2QzZ3JVbEJ1Ym5wYVVXd3ZjR3NLUlZvNGVHOXlka3R1ZW5WcVExTXhiVlpVUW04NWEwSjRiVmxYYnpKRVNIUjVWbmxtWjI1MVQzRldWSHBNV1cxb2J6WlBRMEowV1hkbloySlRUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZHZGpGVENrTjZhVVZMVGpKeVVubHlhbVZXYkVaaVUweG5NUzlSZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJkaFZVZEJNVlZrUlZGRlFpOTNVMEp0YWtOQ2JEUmhRbXhIYURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWHBoVjJSNlpFYzVlUXBhVXpGcVlqSTFiV0l6U25SWlZ6VnFXbE01YkdWSVVubGFWekZzWWtocmRGcEhSblZhTWxaNVlqTldla3hZUWpGWmJYaHdXWGt4ZG1GWFVtcE1WMHBzQ2xsWFRuWmlhVGgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbHNaVWhTZVZwWE1XeGlTR3QwV2tkR2RWb3lWbmxpTTFaNlRGYzVjRnBIVFhRS1dXMVdhRmt5T1hWTWJteDBZa1ZDZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVYSmhTRkl3WTBoTk5ncE1lVGt3WWpKMGJHSnBOV2haTTFKd1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGbVFtZHZja0puUlVWQldVOHZDazFCUlVOQ1FrWXpZak5LY2xwdGVIWmtNVGxyWVZoT2QxbFlVbXBoUkVFeVFtZHZja0puUlVWQldVOHZUVUZGUkVKRGFHcE9Na2w2V2tkYWFVMTZUVEVLV21wQk1VMVhWWGhaZW1jeVdXMVNhRTVIVFROTlZGcHRXVmROTlU0eVVtMU9ha3BvV2tSbmVFMURNRWREYVhOSFFWRlJRbWMzT0hkQlVWRkZTREJXTkFwa1NFcHNZbGRXYzJWVFFtdFpWelZ1V2xoS2RtUllUV2RVTUd4RlVYbENhVnBYUm1waU1qUjNVMUZaUzB0M1dVSkNRVWRFZG5wQlFrSlJVVGRqTW14dUNtTXpVblpqYlZWMFdUSTVkVnB0T1hsaVYwWjFXVEpWZGxwWWFEQmpiVlowV2xkNE5VeFhVbWhpYldSc1kyMDVNV041TVhaaFYxSnFURmRLYkZsWFRuWmlhVFUxWWxkNFFXTnRWbTFqZVRsdkNscFhSbXRqZVRsMFdWZHNkVTFFWjBkRGFYTkhRVkZSUW1jM09IZEJVazFGUzJkM2IxbDZaR2xOTWxKdFdXcE5lazVYV1hkT1ZFWnNUVmROTkU1dFNtc0tXVlJTYWs1NlJUSmFiVVpxVDFSa2ExcHFXWGxaVjFFMFRWUkJhRUpuYjNKQ1owVkZRVmxQTDAxQlJWVkNRazFOUlZoa2RtTnRkRzFpUnpreldESlNjQXBqTTBKb1pFZE9iMDFKUjBKQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtoTlRXTlhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9YcGhWMlI2Q21SSE9YbGFVekZxWWpJMWJXSXpTblJaVnpWcVdsTTViR1ZJVW5sYVZ6RnNZa2hyZEZwSFJuVmFNbFo1WWpOV2VreFlRakZaYlhod1dYa3hkbUZYVW1vS1RGZEtiRmxYVG5aaWFUbG9XVE5TY0dJeU5YcE1NMG94WW01TmRrOUVUVEJPZWxFMFRWUlplVTlET1doa1NGSnNZbGhDTUdONU9IaE5RbGxIUTJselJ3cEJVVkZDWnpjNGQwRlNXVVZEUVhkSFkwaFdhV0pIYkdwTlNVZExRbWR2Y2tKblJVVkJaRm8xUVdkUlEwSklkMFZsWjBJMFFVaFpRVE5VTUhkaGMySklDa1ZVU21wSFVqUmpiVmRqTTBGeFNrdFljbXBsVUVzekwyZzBjSGxuUXpod04yODBRVUZCUjA5V09FRkljR2RCUVVKQlRVRlNla0pHUVdsQ1JtVk5ZbkFLUm1GeWJGQjNZakJ1WVZSeU5HMXFWMFIyV0VGd1QyUTVUMUp4VDJzek5rSnlkRGxUYlhkSmFFRktTblpxYjNJclJGaFZXSEkzVXpOV2JUbHFWa1pVTXdwRFREQkNlR05MUjJvNE5tMDFiVmw2VVhaMVlrMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tZEJUVWRWUTAxQk9HeFVhWGhrVXpScFRqbHRRVlZrZFU5aUNtTlRTbTFvV2t4NWRrczNlbUZZTURWRVRFVkVRMmRRVjNoRVNHc3JTa0phVlV0WlVrbDFTRWhuZDBadVQzZEplRUZNVFdGdGJ6bGtaa1ZPVFhwU1owNEtRM3BaWm5BdmVTdHlVMDlvVm1wWVdFVTViVU51TmtKMVNrVlViSEJTUkdaSGRuaFZaeTgxVEVZNVpqUnNXWEZ2ZWtFOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifX19fQ=="}]}, "messageSignature": {"messageDigest": {"algorithm": "SHA2_256", "digest": "oM/HEnHW4njlfNMy/5V8P3BD/do1TEy7GQow1W76Ab8="}, "signature": "MEUCICYFq/4bTEdlurgqVuNmwCcIWu3NKOCgveWAJBiezJ0uAiEA2i7U18+aRpFxLYksr5HKBQQy08zE050WIc0RzKunDIA="}}'''; +/// The provenance facts of the sample attestation, as persisted at upload. +final _sampleProvenance = VersionProvenance( + repository: 'sigstore-conformance/extremely-dangerous-public-oidc-beacon', + repositoryId: '632596897', + repositoryOwnerId: '131804563', + workflowPath: '.github/workflows/extremely-dangerous-oidc-beacon.yml', + ref: 'refs/heads/main', + commitSha: 'c7b3dfb335f051e1c86bda4c716fac97df62ad81', + runUrl: + 'https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/actions/runs/8347481628/attempts/1', + signer: + 'https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main', + issuer: 'https://token.actions.githubusercontent.com', + transparencyLogIndex: 79571823, + transparencyLogIntegratedTime: DateTime.utc(2024, 3, 19, 17, 26, 26), +); -PackagePageData _createMockData({int? slsaLevel, String? attestationJson}) { +final _sampleSha256 = List.generate( + 32, + (i) => int.parse( + 'a0cfc71271d6e278e57cd332ff957c3f7043fdda354c4cbb190a30d56efa01bf' + .substring(i * 2, i * 2 + 2), + radix: 16, + ), +); + +PackagePageData _createMockData({ + int? slsaLevel, + VersionProvenance? provenance, +}) { final pubspec = Pubspec.fromYaml(''' name: test_pkg version: 1.0.0 @@ -36,32 +62,17 @@ repository: https://github.com/example/test_pkg ..created = DateTime.utc(2026, 1, 1) ..pubspec = pubspec ..uploader = 'test@example.com' - ..slsaLevel = slsaLevel; + ..sha256 = _sampleSha256 + ..slsaLevel = slsaLevel + ..provenance = provenance; final package = Package.fromVersion(version); - final assets = [ - AssetKind.pubspec, - AssetKind.readme, - if (attestationJson != null) AssetKind.attestation, - ]; - - final asset = attestationJson != null - ? (PackageVersionAsset.init( - package: 'test_pkg', - version: '1.0.0', - kind: AssetKind.attestation, - versionCreated: DateTime.utc(2026, 1, 1), - path: 'test_pkg-1.0.0.sigstore.json', - textContent: attestationJson, - )) - : null; - return PackagePageData( package: package, version: version, - assets: assets, - asset: asset, + assets: [AssetKind.pubspec, AssetKind.readme], + asset: null, scoreCard: ScoreCardData(), isAdmin: false, isLiked: false, @@ -70,16 +81,10 @@ repository: https://github.com/example/test_pkg } void main() { - group('AttestationDetails parsing', () { - test('parses Sigstore bundle with certificate and Rekor log', () { - final data = _createMockData( - slsaLevel: 2, - attestationJson: _sampleBundleJson, - ); - final details = AttestationDetails.parse( - data: data, - rawBundleJson: _sampleBundleJson, - ); + group('AttestationDetails', () { + test('is built from the persisted provenance facts', () { + final data = _createMockData(slsaLevel: 2, provenance: _sampleProvenance); + final details = AttestationDetails.fromPageData(data); expect(details, isNotNull); expect(details!.slsaLevel, 2); @@ -88,7 +93,7 @@ void main() { details.rekorUrl, 'https://search.sigstore.dev/?logIndex=79571823', ); - expect(details.rekorTimestamp, isNotNull); + expect(details.rekorTimestamp, DateTime.utc(2024, 3, 19, 17, 26, 26)); expect( details.archiveSha256, 'a0cfc71271d6e278e57cd332ff957c3f7043fdda354c4cbb190a30d56efa01bf', @@ -114,19 +119,28 @@ void main() { expect(details.oidcIssuer, 'https://token.actions.githubusercontent.com'); }); - test('returns null when no attestation and slsaLevel is null', () { + test('falls back to the package links without persisted facts', () { + final data = _createMockData(slsaLevel: 2); + final details = AttestationDetails.fromPageData(data); + expect(details, isNotNull); + expect(details!.slsaLevel, 2); + expect(details.repository, 'https://github.com/example/test_pkg'); + expect(details.signerIdentity, isNull); + expect( + details.archiveSha256, + 'a0cfc71271d6e278e57cd332ff957c3f7043fdda354c4cbb190a30d56efa01bf', + ); + }); + + test('returns null when the version is not attested', () { final data = _createMockData(slsaLevel: null); - final details = AttestationDetails.parse(data: data, rawBundleJson: null); - expect(details, isNull); + expect(AttestationDetails.fromPageData(data), isNull); }); }); group('securityTabNode UI rendering', () { test('renders full Level AAA provenance details when attestation exists', () { - final data = _createMockData( - slsaLevel: 2, - attestationJson: _sampleBundleJson, - ); + final data = _createMockData(slsaLevel: 2, provenance: _sampleProvenance); final node = securityTabNode(data); final html = node.toString();