Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .semgrepignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Byte-exact test fixture corpus: Semgrep findings on curated fixture content
# (e.g., pnpm cooldown policy values inside testdata) are categorically false
# positives. Fixtures mirror real caller files for parser validation and are
# never executed or shipped. Production policy files remain fully scanned.
testdata/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ Human Era five-digit years (e.g., `## [0.1.0] - 12026-06-13`).
feature tables, an expanded security and trust model, badge usage documentation, contributing
links, and a license section.

### Fixed

- Fixed pnpm package resolution for standalone root packages whose `pnpm-workspace.yaml` contains
policy settings but omits the optional `packages` member.

### Security

- Raised the module Go directive to 1.26.5, clearing exposure to
Expand Down
35 changes: 19 additions & 16 deletions internal/fixture/npm_fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,25 @@ func TestNPMFixturesNeverPublish(t *testing.T) {
}

wantCases := map[string]string{
"npm-root-valid": "",
"pnpm-root-valid": "",
"yarn-v4-root-valid": "",
"npm-workspace-valid": "",
"npm-stale-lockfiles-valid": "",
"pnpm-stale-lockfiles-valid": "",
"yarn-stale-lockfiles-valid": "",
"lockfile-only-ambiguous": "windlass.verify.error.package-manager-conflict",
"selected-lockfile-missing": "windlass.verify.error.required-lockfile-missing",
"pnpm-version-missing": "windlass.verify.error.package-manager-version-required",
"yarn-classic-rejected": "windlass.verify.error.yarn-selection-invalid",
"yarn-devengines-rejected": "windlass.verify.error.yarn-selection-invalid",
"private-package-rejected": "windlass.verify.error.package-private",
"package-metadata-missing-rejected": "windlass.verify.error.package-metadata-required",
"unsupported-manager-rejected": "windlass.verify.error.package-manager-conflict",
"yarn-lockfile-only-rejected": "windlass.verify.error.yarn-selection-invalid",
"npm-root-valid": "",
"pnpm-root-valid": "",
"pnpm-settings-only-root-valid": "",
"yarn-v4-root-valid": "",
"npm-workspace-valid": "",
"npm-stale-lockfiles-valid": "",
"pnpm-stale-lockfiles-valid": "",
"yarn-stale-lockfiles-valid": "",
"lockfile-only-ambiguous": "windlass.verify.error.package-manager-conflict",
"selected-lockfile-missing": "windlass.verify.error.required-lockfile-missing",
"pnpm-version-missing": "windlass.verify.error.package-manager-version-required",
"yarn-classic-rejected": "windlass.verify.error.yarn-selection-invalid",
"yarn-devengines-rejected": "windlass.verify.error.yarn-selection-invalid",
"private-package-rejected": "windlass.verify.error.package-private",
"package-metadata-missing-rejected": "windlass.verify.error.package-metadata-required",
"unsupported-manager-rejected": "windlass.verify.error.package-manager-conflict",
"yarn-lockfile-only-rejected": "windlass.verify.error.yarn-selection-invalid",
"pnpm-workspace-packages-wrong-type": "windlass.verify.error.package-resolution-invalid",
"pnpm-settings-only-subdirectory": "windlass.verify.error.package-resolution-invalid",
}

for _, manifest := range index.Fixtures {
Expand Down
19 changes: 11 additions & 8 deletions internal/fixture/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var requirementRegistry = map[string]string{
"ADR-0017.exact-package-manager-version": "pnpm and Yarn require exact release versions",
"ADR-0018.single-workspace-package": "one explicitly selected workspace package per run",
"ADR-0019.package-publish-metadata": "package identity and private publish intent validation",
"ADR-0078.pnpm-settings-only-root-package": "settings-only pnpm metadata selects only the standalone root package",
"ADR-0056.stale-lockfile-diagnostics": "non-selected lockfiles are recorded stale diagnostics",
"ADR-0056.stale-lockfile-rejections": "ambiguous and missing selected lockfiles are rejected",
"ADR-0063.yarn-v4-selection": "Yarn requires top-level exact Berry v4 packageManager metadata",
Expand All @@ -28,21 +29,23 @@ var failureCategoryRegistry = map[string]struct{}{
"package-manager-version-required": {},
"package-metadata-required": {},
"package-private": {},
"package-resolution-invalid": {},
"policy-schema-invalid": {},
"required-lockfile-missing": {},
"yarn-selection-invalid": {},
}

var fixturePhaseRequirements = map[string]map[string]struct{}{
"build-pack": {
"ADR-0014.package-manager-matrix": {},
"ADR-0017.exact-package-manager-version": {},
"ADR-0018.single-workspace-package": {},
"ADR-0019.package-publish-metadata": {},
"ADR-0056.stale-lockfile-diagnostics": {},
"ADR-0056.stale-lockfile-rejections": {},
"ADR-0063.yarn-v4-selection": {},
"ADR-0064.npm-purl-subject-digests": {},
"ADR-0014.package-manager-matrix": {},
"ADR-0017.exact-package-manager-version": {},
"ADR-0018.single-workspace-package": {},
"ADR-0019.package-publish-metadata": {},
"ADR-0056.stale-lockfile-diagnostics": {},
"ADR-0056.stale-lockfile-rejections": {},
"ADR-0063.yarn-v4-selection": {},
"ADR-0064.npm-purl-subject-digests": {},
"ADR-0078.pnpm-settings-only-root-package": {},
},
"provenance-inputs": {
"ARCH-js-ts-npm-provenance-publish.signing-inputs": {},
Expand Down
19 changes: 19 additions & 0 deletions internal/npmprofile/selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ func TestWorkspaceDiscovery(t *testing.T) {
t.Fatalf("manager selection = %#v", result.Manager)
}

t.Run("pnpm settings-only root", func(t *testing.T) {
t.Parallel()
result := analyzeFixture(t, testRepositoryRoot(t), "testdata/npm/packages/pnpm-settings-only-valid")
assertPass(t, result)
if result.Package.Directory != "." || result.Package.ManagerRoot != "." {
t.Fatalf("standalone root package = %#v", result.Package)
}
})

t.Run("pnpm settings-only subdirectory fails closed", func(t *testing.T) {
t.Parallel()
result := analyzeFixture(t, testRepositoryRoot(t), "testdata/npm/packages/rejected/pnpm-settings-only-subdirectory/packages/undeclared")
assertRejected(t, result, IDPackageResolutionInvalid)
})

t.Run("pnpm recursive pattern", func(t *testing.T) {
t.Parallel()
root := createRepository(t, map[string]string{
Expand Down Expand Up @@ -343,6 +358,10 @@ func analyzeFixture(t *testing.T, repositoryRoot, packageDirectory string) Resul
}
if len(parts) > 0 && parts[0] == "rejected" {
fixtureRootParts = 2
selectedDirectory = strings.Join(parts[2:], "/")
if selectedDirectory == "" {
selectedDirectory = "."
}
}
if len(parts) > 0 && parts[0] == "workspace-valid" {
fixtureRootParts = 1
Expand Down
40 changes: 24 additions & 16 deletions internal/npmprofile/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func discoverWorkspaceRoot(repositoryRoot, selectedDirectory string) (string, *manifest, error) {
for candidate := selectedDirectory; ; candidate = filepath.Dir(candidate) {
patterns, candidateManifest, metadataPresent, err := workspacePatterns(candidate)
patterns, candidateManifest, metadataPresent, pnpmRootOnly, err := workspacePatterns(candidate)
if err != nil {
return "", nil, err
}
Expand All @@ -22,6 +22,12 @@ func discoverWorkspaceRoot(repositoryRoot, selectedDirectory string) (string, *m
if relErr != nil {
return "", nil, relErr
}
if pnpmRootOnly {
if relative == "." {
return candidate, candidateManifest, nil
}
return "", nil, errors.New("settings-only pnpm workspace cannot claim a subdirectory")
}
matched := false
for _, pattern := range patterns {
if matchWorkspacePattern(pattern, canonicalRelative(relative)) {
Expand All @@ -43,48 +49,50 @@ func discoverWorkspaceRoot(repositoryRoot, selectedDirectory string) (string, *m
return selectedDirectory, nil, nil
}

func workspacePatterns(candidate string) ([]string, *manifest, bool, error) {
func workspacePatterns(candidate string) ([]string, *manifest, bool, bool, error) {
var patterns []string
var candidateManifest *manifest
metadataPresent := false
pnpmRootOnly := false

manifestPath := filepath.Join(candidate, "package.json")
if _, err := os.Lstat(manifestPath); err == nil {
decoded, _, readErr := readManifest(manifestPath)
if readErr != nil {
return nil, nil, false, readErr
return nil, nil, false, false, readErr
}
candidateManifest = &decoded
if len(decoded.Workspaces) != 0 {
metadataPresent = true
workspacePatterns, parseErr := parseJSONWorkspacePatterns(decoded.Workspaces)
if parseErr != nil {
return nil, nil, false, parseErr
return nil, nil, false, false, parseErr
}
patterns = append(patterns, workspacePatterns...)
}
} else if !os.IsNotExist(err) {
return nil, nil, false, err
return nil, nil, false, false, err
}

pnpmPath := filepath.Join(candidate, "pnpm-workspace.yaml")
if encoded, fileErr := readRegularNonSymlink(pnpmPath); fileErr == nil {
metadataPresent = true
pnpmPatterns, parseErr := parsePNPMWorkspacePatterns(encoded)
pnpmPatterns, rootOnly, parseErr := parsePNPMWorkspacePatterns(encoded)
if parseErr != nil {
return nil, nil, false, parseErr
return nil, nil, false, false, parseErr
}
pnpmRootOnly = rootOnly
patterns = append(patterns, pnpmPatterns...)
} else if !os.IsNotExist(fileErr) {
return nil, nil, false, fileErr
return nil, nil, false, false, fileErr
}

for _, pattern := range patterns {
if err := validateWorkspacePattern(pattern); err != nil {
return nil, nil, false, err
return nil, nil, false, false, err
}
}
return patterns, candidateManifest, metadataPresent, nil
return patterns, candidateManifest, metadataPresent, pnpmRootOnly, nil
}

func parseJSONWorkspacePatterns(raw jsonRaw) ([]string, error) {
Expand All @@ -103,29 +111,29 @@ func parseJSONWorkspacePatterns(raw jsonRaw) ([]string, error) {
return direct, nil
}

func parsePNPMWorkspacePatterns(encoded []byte) ([]string, error) {
func parsePNPMWorkspacePatterns(encoded []byte) ([]string, bool, error) {
var object map[string]any
decoder := yaml.NewDecoder(bytes.NewReader(encoded), yaml.Strict())
if err := decoder.Decode(&object); err != nil || object == nil {
return nil, errors.New("pnpm workspace must be an object")
return nil, false, errors.New("pnpm workspace must be an object")
}
rawPackages, ok := object["packages"]
if !ok {
return nil, errors.New("pnpm workspace packages are required")
return nil, true, nil
}
values, ok := rawPackages.([]any)
if !ok {
return nil, errors.New("pnpm workspace packages must be an array")
return nil, false, errors.New("pnpm workspace packages must be an array")
}
patterns := make([]string, 0, len(values))
for _, value := range values {
pattern, ok := value.(string)
if !ok {
return nil, errors.New("pnpm workspace patterns must be strings")
return nil, false, errors.New("pnpm workspace patterns must be strings")
}
patterns = append(patterns, pattern)
}
return patterns, nil
return patterns, false, nil
}

func validateWorkspacePattern(pattern string) error {
Expand Down
39 changes: 39 additions & 0 deletions testdata/fixtures/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@
"expected-secondary-ids": [],
"covered-requirement": "ADR-0064.npm-purl-subject-digests"
},
{
"name": "pnpm-settings-only-root-valid",
"type": "accepted",
"surface": "npm",
"artifact": "testdata/npm/packages/pnpm-settings-only-valid/package.json",
"provenance": "testdata/npm/packages/pnpm-settings-only-valid/fixture.json",
"release-manifest": null,
"expected-result": "pass",
"expected-failure-category": null,
"expected-primary-id": null,
"expected-secondary-ids": [],
"covered-requirement": "ADR-0078.pnpm-settings-only-root-package"
},
{
"name": "npm-provenance-predicate-jcs-valid",
"type": "accepted",
Expand Down Expand Up @@ -233,6 +246,32 @@
"expected-primary-id": "windlass.verify.error.yarn-selection-invalid",
"expected-secondary-ids": [],
"covered-requirement": "ADR-0063.yarn-v4-selection"
},
{
"name": "pnpm-workspace-packages-wrong-type",
"type": "rejected",
"surface": "npm",
"artifact": "testdata/npm/packages/rejected/pnpm-workspace-packages-wrong-type/package.json",
"provenance": "testdata/npm/packages/rejected/pnpm-workspace-packages-wrong-type/fixture.json",
"release-manifest": null,
"expected-result": "fail",
"expected-failure-category": "package-resolution-invalid",
"expected-primary-id": "windlass.verify.error.package-resolution-invalid",
"expected-secondary-ids": [],
"covered-requirement": "ADR-0078.pnpm-settings-only-root-package"
},
{
"name": "pnpm-settings-only-subdirectory",
"type": "rejected",
"surface": "npm",
"artifact": "testdata/npm/packages/rejected/pnpm-settings-only-subdirectory/packages/undeclared/package.json",
"provenance": "testdata/npm/packages/rejected/pnpm-settings-only-subdirectory/fixture.json",
"release-manifest": null,
"expected-result": "fail",
"expected-failure-category": "package-resolution-invalid",
"expected-primary-id": "windlass.verify.error.package-resolution-invalid",
"expected-secondary-ids": [],
"covered-requirement": "ADR-0078.pnpm-settings-only-root-package"
}
]
}
18 changes: 18 additions & 0 deletions testdata/npm/packages/pnpm-settings-only-valid/fixture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"phase": "build-pack",
"package-directory": "testdata/npm/packages/pnpm-settings-only-valid",
"package-manager": {
"name": "pnpm",
"version": "10.14.0",
"selection-source": "packageManager",
"selected-lockfile": "pnpm-lock.yaml",
"distribution-digest-authority": "registry-integrity"
},
"commands": {
"install": ["pnpm", "install", "--frozen-lockfile"],
"build": [],
"pack": ["pnpm", "pack", "--pack-destination", "<pack-output-directory>"]
},
"expected-subject": "pkg:npm/%40windlass-fixtures/pnpm-settings-only@1.0.0",
"required-digests": ["sha512", "sha256"]
}
8 changes: 8 additions & 0 deletions testdata/npm/packages/pnpm-settings-only-valid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@windlass-fixtures/pnpm-settings-only",
"version": "1.0.0",
"description": "Standalone pnpm package with workspace policy settings",
"license": "Apache-2.0",
"repository": "github:windlasstech/slsa-builder",
"packageManager": "pnpm@10.14.0"
}
8 changes: 8 additions & 0 deletions testdata/npm/packages/pnpm-settings-only-valid/pnpm-lock.yaml

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
minimumReleaseAge: 1440

@semgrep-code-windlasstech semgrep-code-windlasstech Bot Aug 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pnpm workspace configuration does not set a minimum release age. Newly published packages can be malicious or unstable. Add minimumReleaseAge: 10080 (minutes) to wait at least seven days before installing newly published package versions. Added in: v10.16.0 Reference: https://pnpm.io/settings#minimumreleaseage

🥳 Removed in commit 8d4245e 🥳

minimumReleaseAgeExclude:
- '@windlass-fixtures/*'
onlyBuiltDependencies:
- esbuild
trustPolicy: no-downgrade
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"phase":"build-pack","package-directory":"testdata/npm/packages/rejected/pnpm-settings-only-subdirectory/packages/undeclared","commands":{},"expected-primary-id":"windlass.verify.error.package-resolution-invalid"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@windlass-fixtures/pnpm-settings-only-root",
"version": "1.0.0",
"private": true,
"repository": "github:windlasstech/slsa-builder",
"packageManager": "pnpm@10.14.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@windlass-fixtures/undeclared-subdirectory",
"version": "1.0.0",
"repository": "github:windlasstech/slsa-builder"
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
minimumReleaseAge: 1440

@semgrep-code-windlasstech semgrep-code-windlasstech Bot Aug 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pnpm workspace configuration does not set a minimum release age. Newly published packages can be malicious or unstable. Add minimumReleaseAge: 10080 (minutes) to wait at least seven days before installing newly published package versions. Added in: v10.16.0 Reference: https://pnpm.io/settings#minimumreleaseage

✨ Removed in commit 8d4245e ✨

minimumReleaseAgeExclude:
- '@windlass-fixtures/*'
onlyBuiltDependencies:
- esbuild
trustPolicy: no-downgrade
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"phase":"build-pack","package-directory":"testdata/npm/packages/rejected/pnpm-workspace-packages-wrong-type","commands":{},"expected-primary-id":"windlass.verify.error.package-resolution-invalid"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@windlass-fixtures/pnpm-workspace-packages-wrong-type",
"version": "1.0.0",
"repository": "github:windlasstech/slsa-builder",
"packageManager": "pnpm@10.14.0"
}

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

Loading