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
44 changes: 28 additions & 16 deletions internal/npm/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ func (r *Registry) URLs() core.URLBuilder { //nolint:ireturn
}

type packageResponse struct {
ID string `json:"_id"`
Name string `json:"name"`
Description string `json:"description"`
Homepage interface{} `json:"homepage"`
Repository interface{} `json:"repository"`
Versions map[string]versionInfo `json:"versions"`
Time map[string]string `json:"time"`
Maintainers []maintainerInfo `json:"maintainers"`
DistTags map[string]string `json:"dist-tags"`
ID string `json:"_id"`
Name string `json:"name"`
Description string `json:"description"`
Homepage interface{} `json:"homepage"`
Repository interface{} `json:"repository"`
Versions map[string]versionInfo `json:"versions"`
Time map[string]string `json:"time"`
Maintainers []maintainerInfo `json:"maintainers"`
DistTags map[string]string `json:"dist-tags"`
}

type versionInfo struct {
Expand All @@ -79,6 +79,17 @@ type versionInfo struct {
NpmUser map[string]interface{} `json:"_npmUser"`
Engines interface{} `json:"engines"`
Funding interface{} `json:"funding"`

ContentPolicy *ContentPolicy `json:"contentPolicy"`
}

// ContentPolicy is the npm package.json "contentPolicy" field, propagated
// into the packument version object. Maintainers set it to declare
// dual-use content (security-relevant capabilities that automated
// scanning could otherwise flag as malicious). See
// https://docs.npmjs.com/policies/dual-use.
type ContentPolicy struct {
Class string `json:"class"` // "dual-use" is the only value npm defines today
}

// deprecatedField is the npm version "deprecated" field, which the packument
Expand Down Expand Up @@ -231,13 +242,14 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi
Integrity: integrity,
Status: status,
Metadata: map[string]any{
"deprecated": string(v.Deprecated),
"dist": v.Dist,
"engines": v.Engines,
"_npmUser": v.NpmUser,
"tarball": v.Dist.Tarball,
"npm:attestations": v.Dist.Attestations,
"npm:signatures": v.Dist.Signatures,
"deprecated": string(v.Deprecated),
"dist": v.Dist,
"engines": v.Engines,
"_npmUser": v.NpmUser,
"tarball": v.Dist.Tarball,
"npm:attestations": v.Dist.Attestations,
"npm:signatures": v.Dist.Signatures,
"npm:contentPolicy": v.ContentPolicy,
},
})
}
Expand Down
47 changes: 47 additions & 0 deletions internal/npm/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,53 @@ func TestFetchVersions_NoProvenance(t *testing.T) {
if sigs, _ := versions[0].Metadata["npm:signatures"].([]Signature); len(sigs) != 0 {
t.Errorf("expected empty signatures, got %+v", sigs)
}
cpAny, ok := versions[0].Metadata["npm:contentPolicy"]
if !ok {
t.Fatalf("expected Metadata[npm:contentPolicy] key to be present")
}
cp, ok := cpAny.(*ContentPolicy)
if !ok || cp != nil {
t.Errorf("expected nil *ContentPolicy, got %T (%+v)", cpAny, cp)
}
}

// TestFetchVersions_ContentPolicy asserts the package.json contentPolicy
// field round-trips into Version.Metadata as a typed *ContentPolicy.
func TestFetchVersions_ContentPolicy(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
resp := map[string]interface{}{
"_id": "dualuse",
"name": "dualuse",
"dist-tags": map[string]string{"latest": "1.0.0"},
"versions": map[string]interface{}{
"1.0.0": map[string]interface{}{
"name": "dualuse",
"version": "1.0.0",
"contentPolicy": map[string]string{"class": "dual-use"},
"dist": map[string]interface{}{"integrity": "sha512-xxx", "tarball": "https://example.invalid/dualuse-1.0.0.tgz"},
},
},
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()

reg := New(server.URL, core.DefaultClient())
versions, err := reg.FetchVersions(context.Background(), "dualuse")
if err != nil {
t.Fatalf("FetchVersions: %v", err)
}
if len(versions) != 1 {
t.Fatalf("versions = %d, want 1", len(versions))
}

cp, ok := versions[0].Metadata["npm:contentPolicy"].(*ContentPolicy)
if !ok || cp == nil {
t.Fatalf("Metadata[npm:contentPolicy] not a *ContentPolicy: %T", versions[0].Metadata["npm:contentPolicy"])
}
if cp.Class != "dual-use" {
t.Errorf("contentPolicy.class = %q, want %q", cp.Class, "dual-use")
}
}

// TestFetchVersions_LegacyEnginesArray verifies that versions whose
Expand Down