Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Many of the assessments depend upon the presence of a [Security Insights](https:

## Work in Progress

Currently 42 control requirements across OSPS Baselines levels 1-3 are covered, with 10 not yet implemented. [Maturity Level 1](https://baseline.openssf.org/versions/2025-02-25.html#level-1) requirements are the most rigorously tested and are recommended for use. The results of these layer 1 assessments are integrated into [LFX Insights](https://insights.linuxfoundation.org/project/k8s/repository/kubernetes-kubernetes/security), powering the [Security & Best Practices results](https://insights.linuxfoundation.org/docs/metrics/security/).
Currently 43 control requirements across OSPS Baselines levels 1-3 are covered, with 9 not yet implemented. [Maturity Level 1](https://baseline.openssf.org/versions/2025-02-25.html#level-1) requirements are the most rigorously tested and are recommended for use. The results of these layer 1 assessments are integrated into [LFX Insights](https://insights.linuxfoundation.org/project/k8s/repository/kubernetes-kubernetes/security), powering the [Security & Best Practices results](https://insights.linuxfoundation.org/docs/metrics/security/).

![alt text](kubernetes_insights_baseline.png)

Expand Down
2 changes: 1 addition & 1 deletion evaluation_plans/evaluation-plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ var (
build_release.ReleaseHasUniqueIdentifier,
},
"OSPS-BR-02.02": {
reusable_steps.NotImplemented,
build_release.ReleaseAssetsAssociatedWithRelease,
},
"OSPS-BR-03.01": {
build_release.EnsureInsightsLinksUseHTTPS,
Expand Down
146 changes: 146 additions & 0 deletions evaluation_plans/osps/build_release/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/rhysd/actionlint"

"github.com/ossf/pvtr-github-repo-scanner/data"
"github.com/ossf/pvtr-github-repo-scanner/evaluation_plans/reusable_steps"
)

// Pre-compiled patterns used by workflow security checks.
Expand Down Expand Up @@ -494,6 +495,151 @@ func ReleaseHasUniqueIdentifier(payload data.Payload) (result gemara.Result, mes
return gemara.Passed, "All releases found have a unique name", confidence
}

// releaseAssetCompanionSuffixes identify signature, checksum, attestation, and
// SBOM companion files. A companion is associated with the artifact it
// accompanies rather than carrying the release identifier itself, so it is
// exempt from the name-association expectation.
var releaseAssetCompanionSuffixes = []string{
".sig", ".asc", ".pem", ".minisig",
".sha1", ".sha256", ".sha512", ".md5",
".intoto.jsonl", ".sbom", ".spdx.json", ".cdx.json",
}

// releaseAssetCompanionNames are exact (lowercased) asset names that accompany
// a release without identifying a specific artifact: checksum manifests and
// standard documentation files.
var releaseAssetCompanionNames = map[string]bool{
"checksums.txt": true, "sha256sums": true, "sha512sums": true, "md5sums": true,
"license": true, "license.txt": true, "license.md": true,
"readme": true, "readme.txt": true, "readme.md": true,
}

// isReleaseAssetCompanion reports whether a lowercased asset name is a
// companion file rather than a release artifact in its own right.
func isReleaseAssetCompanion(lowerName string) bool {
if releaseAssetCompanionNames[lowerName] {
return true
}
for _, suffix := range releaseAssetCompanionSuffixes {
if strings.HasSuffix(lowerName, suffix) {
return true
}
}
return false
}

// releaseIdentifierCandidates returns the lowercased identifier strings an
// asset name may embed to associate itself with the release: the tag, the tag
// without its leading "v" (kept only when at least two characters so a single
// digit cannot match almost any name), and the release name when it contains
// no spaces (asset file names cannot contain a spaced title).
func releaseIdentifierCandidates(release data.ReleaseData) []string {
var candidates []string
tag := strings.ToLower(strings.TrimSpace(release.TagName))
if tag != "" {
candidates = append(candidates, tag)
if trimmed := strings.TrimPrefix(tag, "v"); trimmed != tag && len(trimmed) >= 2 {
candidates = append(candidates, trimmed)
}
}
if name := strings.ToLower(strings.TrimSpace(release.Name)); name != "" && !strings.Contains(name, " ") {
candidates = append(candidates, name)
}
return candidates
}

// assetNameContainsAny reports whether a lowercased asset name embeds any of
// the candidate identifier strings.
func assetNameContainsAny(lowerName string, candidates []string) bool {
for _, candidate := range candidates {
if strings.Contains(lowerName, candidate) {
return true
}
}
return false
}

// releaseAssetLabel names a release in evidence messages, preferring the tag.
func releaseAssetLabel(release data.ReleaseData) string {
if release.TagName != "" {
return release.TagName
}
if release.Name != "" {
return release.Name
}
return "(unnamed)"
}

// ReleaseAssetsAssociatedWithRelease checks that when an official release is
// created, all assets within that release are clearly associated with the
// release identifier or another unique identifier for the asset.
//
// GitHub structurally attaches every asset to its release, so the observable
// question is whether each asset's name also carries the release identifier
// (tag, tag without the leading v, or release name), the convention the
// baseline's recommendation describes. Signature, checksum, attestation, and
// SBOM companions plus standard documentation files are exempt: they are
// associated through the artifact they accompany. An asset without a
// recognizable identifier is not proof of a violation, because "another unique
// identifier for the asset" may come from a scheme the scanner cannot observe,
// so such assets surface as NeedsReview rather than Failed.
//
// - NeedsReview: release data unobservable, at least one non-companion asset
// does not embed a release identifier, or only companion files are
// published.
// - NotApplicable: no published releases, or no release publishes assets.
// - Passed: every non-companion asset embeds a release identifier.
func ReleaseAssetsAssociatedWithRelease(payload data.Payload) (gemara.Result, string, gemara.ConfidenceLevel) {
released, observable := reusable_steps.HasPublishedRelease(payload)
if !observable {
return gemara.NeedsReview, "Release data is unavailable; manually review whether release assets are associated with the release identifier", gemara.Low
}
if !released {
return gemara.NotApplicable, "No published releases found; the asset-identifier requirement does not apply", gemara.High
}

totalAssets := 0
checkedAssets := 0
var unassociated []string
for _, release := range payload.Releases {
if release.Draft {
continue
}
candidates := releaseIdentifierCandidates(release)
for _, asset := range release.Assets {
totalAssets++
lower := strings.ToLower(strings.TrimSpace(asset.Name))
if lower == "" || isReleaseAssetCompanion(lower) {
continue
}
checkedAssets++
if !assetNameContainsAny(lower, candidates) {
unassociated = append(unassociated, fmt.Sprintf("%s (release %s)", asset.Name, releaseAssetLabel(release)))
}
}
}

if totalAssets == 0 {
return gemara.NotApplicable, "Published releases have no attached assets; the asset-identifier requirement does not apply", gemara.Low
}
if len(unassociated) > 0 {
const maxListedAssets = 5
listed := unassociated
overflow := ""
if len(listed) > maxListedAssets {
overflow = fmt.Sprintf(" and %d more", len(listed)-maxListedAssets)
listed = listed[:maxListedAssets]
}
return gemara.NeedsReview, fmt.Sprintf(
"%d release asset(s) do not embed a release identifier in their name: %s%s. They may be associated through another identifier scheme; manual review required",
len(unassociated), strings.Join(listed, ", "), overflow), gemara.Low
}
if checkedAssets == 0 {
return gemara.NeedsReview, "Published release assets are only companion files (checksums, signatures, documentation); manually review how release artifacts are identified", gemara.Low
}
return gemara.Passed, "All release assets embed a release identifier (tag or release name) in their file name", gemara.Medium
}

func getLinks(payload data.Payload) []string {
ins := payload.Insights
var links []string
Expand Down
Loading