Skip to content

feat: support fetching snapshot versions from a Maven registry #1160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Aug 27, 2024
88 changes: 77 additions & 11 deletions internal/resolution/datasource/maven_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,97 @@ func NewMavenRegistryAPIClient(registry string) *MavenRegistryAPIClient {

var errAPIFailed = errors.New("API query failed")

// GetProject fetches a pom.xml specified by groupID, artifactID and version and parses it to maven.Project.
// For a snapshot version, version level metadata is used to find the extact version string.
// More about Maven Repository Metadata Model: https://maven.apache.org/ref/3.9.9/maven-repository-metadata/
// More about Maven Metadata: https://maven.apache.org/repositories/metadata.html
func (m *MavenRegistryAPIClient) GetProject(ctx context.Context, groupID, artifactID, version string) (maven.Project, error) {
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, version, fmt.Sprintf("%s-%s.pom", artifactID, version))
if !strings.HasSuffix(version, "-SNAPSHOT") {
return m.getProject(ctx, groupID, artifactID, version, "")
}

// Fetch version metadata for snapshot versions.
metadata, err := m.getVersionMetadata(ctx, groupID, artifactID, version)
if err != nil {
return maven.Project{}, err
}
Comment on lines +37 to +40
Copy link
Member

Choose a reason for hiding this comment

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

Is it valid for a snapshot version directory to not have a maven-metadata.xml file? If it is, would maven just look for pkg-1.2.3-SNAPSHOT.pom?

Conversely, is it possible for a non-snapshot version to have a metadata file with snapshotVersions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure if it is valid for a snapshot version to not have a metadata file.

Based on the link in the comment above, a non-snapshot version should not have a metadata file with snapshotVersion.


snapshot := ""
for _, sv := range metadata.Versioning.SnapshotVersions {
if sv.Extension == "pom" {
// We only look for pom.xml for project metadata.
Comment on lines +44 to +45
Copy link
Member

Choose a reason for hiding this comment

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

Do you know what happens if the pom and jar versions are mismatched?

e.g. foo-1.0.0-SNAPSHOT has jar = 1.0.0-1 and pom = 1.0.0-2.
Presumably, maven would download foo-1.0.0-1.jar for my dependency, but would it look for foo-1.0.0-1.pom or foo-1.0.0-2.pom for its dependencies?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know exact answer to this question. A snapshot version is the version in development before release and the actual version string is the timestamp + build number - I would expect them to be the same for the same snapshot version.

snapshot = string(sv.Value)
break
}
}

return m.getProject(ctx, groupID, artifactID, version, snapshot)
}

// getProject fetches a pom.xml specified by groupID, artifactID and version and parses it to maven.Project.
// For snapshot versions, the exact version value is specified by snapshot.
func (m *MavenRegistryAPIClient) getProject(ctx context.Context, groupID, artifactID, version, snapshot string) (maven.Project, error) {
if snapshot == "" {
snapshot = version
}
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, version, fmt.Sprintf("%s-%s.pom", artifactID, snapshot))
if err != nil {
return maven.Project{}, fmt.Errorf("failed to join path: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
var proj maven.Project
if err := get(ctx, u, &proj); err != nil {
return maven.Project{}, err
}

return proj, nil
}

// getVersionMetadata fetches a version level maven-metadata.xml and parses it to maven.Metadata.
func (m *MavenRegistryAPIClient) getVersionMetadata(ctx context.Context, groupID, artifactID, version string) (maven.Metadata, error) {
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, version, "maven-metadata.xml")
if err != nil {
return maven.Metadata{}, fmt.Errorf("failed to join path: %w", err)
}

var metadata maven.Metadata
if err := get(ctx, u, &metadata); err != nil {
return maven.Metadata{}, err
}

return metadata, nil
}

// GetArtifactMetadata fetches an artifact level maven-metadata.xml and parses it to maven.Metadata.
func (m *MavenRegistryAPIClient) GetArtifactMetadata(ctx context.Context, groupID, artifactID string) (maven.Metadata, error) {
u, err := url.JoinPath(m.registry, strings.ReplaceAll(groupID, ".", "/"), artifactID, "maven-metadata.xml")
if err != nil {
return maven.Project{}, fmt.Errorf("failed to make new request: %w", err)
return maven.Metadata{}, fmt.Errorf("failed to join path: %w", err)
}

var metadata maven.Metadata
if err := get(ctx, u, &metadata); err != nil {
return maven.Metadata{}, err
}

return metadata, nil
}

func get(ctx context.Context, url string, dst interface{}) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to make new request: %w", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return maven.Project{}, fmt.Errorf("%w: Maven registry query failed: %w", errAPIFailed, err)
return fmt.Errorf("%w: Maven registry query failed: %w", errAPIFailed, err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return maven.Project{}, fmt.Errorf("%w: Maven registry query status: %s", errAPIFailed, resp.Status)
return fmt.Errorf("%w: Maven registry query status: %s", errAPIFailed, resp.Status)
}

var proj maven.Project
if err := xml.NewDecoder(resp.Body).Decode(&proj); err != nil {
return maven.Project{}, fmt.Errorf("failed to decode Maven project: %w", err)
}

return proj, nil
return xml.NewDecoder(resp.Body).Decode(dst)
}
207 changes: 207 additions & 0 deletions internal/resolution/datasource/maven_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package datasource

import (
"context"
"reflect"
"testing"

"deps.dev/util/maven"
"github.com/google/osv-scanner/internal/testutility"
)

func TestGetProject(t *testing.T) {
t.Parallel()

srv := testutility.NewMockHTTPServer(t)
client := &MavenRegistryAPIClient{
registry: srv.URL,
}
srv.SetResponse(t, "org/example/x.y.z/1.0.0/x.y.z-1.0.0.pom", []byte(`
<project>
<groupId>org.example</groupId>
<artifactId>x.y.z</artifactId>
<version>1.0.0</version>
</project>
`))

got, err := client.GetProject(context.Background(), "org.example", "x.y.z", "1.0.0")
if err != nil {
t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "1.0.0", err)
}
want := maven.Project{
ProjectKey: maven.ProjectKey{
GroupID: "org.example",
ArtifactID: "x.y.z",
Version: "1.0.0",
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "1.0.0", got, want)
}
}

func TestGetProjectSnapshot(t *testing.T) {
t.Parallel()

srv := testutility.NewMockHTTPServer(t)
client := &MavenRegistryAPIClient{
registry: srv.URL,
}
srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/maven-metadata.xml", []byte(`
<metadata>
<groupId>org.example</groupId>
<artifactId>x.y.z</artifactId>
<versioning>
<snapshot>
<timestamp>20230302.052731</timestamp>
<buildNumber>9</buildNumber>
</snapshot>
<lastUpdated>20230302052731</lastUpdated>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>3.3.1-20230302.052731-9</value>
<updated>20230302052731</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<value>3.3.1-20230302.052731-9</value>
<updated>20230302052731</updated>
</snapshotVersion>
</snapshotVersions>
</versioning>
</metadata>
`))
srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/x.y.z-3.3.1-20230302.052731-9.pom", []byte(`
<project>
<groupId>org.example</groupId>
<artifactId>x.y.z</artifactId>
<version>3.3.1-SNAPSHOT</version>
</project>
`))

got, err := client.GetProject(context.Background(), "org.example", "x.y.z", "3.3.1-SNAPSHOT")
if err != nil {
t.Fatalf("failed to get Maven project %s:%s verion %s: %v", "org.example", "x.y.z", "3.3.1-SNAPSHOT", err)
}
want := maven.Project{
ProjectKey: maven.ProjectKey{
GroupID: "org.example",
ArtifactID: "x.y.z",
Version: "3.3.1-SNAPSHOT",
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("GetProject(%s, %s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", "3.3.1-SNAPSHOT", got, want)
}
}

func TestGetArtifactMetadata(t *testing.T) {
t.Parallel()

srv := testutility.NewMockHTTPServer(t)
client := &MavenRegistryAPIClient{
registry: srv.URL,
}
srv.SetResponse(t, "org/example/x.y.z/maven-metadata.xml", []byte(`
<metadata>
<groupId>org.example</groupId>
<artifactId>x.y.z</artifactId>
<versioning>
<latest>3.0</latest>
<release>3.0</release>
<versions>
<version>1.0</version>
<version>2.0</version>
<version>3.0</version>
</versions>
</versioning>
</metadata>
`))

got, err := client.GetArtifactMetadata(context.Background(), "org.example", "x.y.z")
if err != nil {
t.Fatalf("failed to get artifact metadata for %s:%s: %v", "org.example", "x.y.z", err)
}
want := maven.Metadata{
GroupID: "org.example",
ArtifactID: "x.y.z",
Versioning: maven.Versioning{
Latest: "3.0",
Release: "3.0",
Versions: []maven.String{
"1.0",
"2.0",
"3.0",
},
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("GetArtifactMetadata(%s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", got, want)
}
}

func TestGetVersionMetadata(t *testing.T) {
t.Parallel()

srv := testutility.NewMockHTTPServer(t)
client := &MavenRegistryAPIClient{
registry: srv.URL,
}
srv.SetResponse(t, "org/example/x.y.z/3.3.1-SNAPSHOT/maven-metadata.xml", []byte(`
<metadata>
<groupId>org.example</groupId>
<artifactId>x.y.z</artifactId>
<versioning>
<snapshot>
<timestamp>20230302.052731</timestamp>
<buildNumber>9</buildNumber>
</snapshot>
<lastUpdated>20230302052731</lastUpdated>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<value>3.3.1-20230302.052731-9</value>
<updated>20230302052731</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<value>3.3.1-20230302.052731-9</value>
<updated>20230302052731</updated>
</snapshotVersion>
</snapshotVersions>
</versioning>
</metadata>
`))

got, err := client.getVersionMetadata(context.Background(), "org.example", "x.y.z", "3.3.1-SNAPSHOT")
if err != nil {
t.Fatalf("failed to get metadata for %s:%s verion %s: %v", "org.example", "x.y.z", "3.3.1-SNAPSHOT", err)
}
want := maven.Metadata{
GroupID: "org.example",
ArtifactID: "x.y.z",
Versioning: maven.Versioning{
Snapshot: maven.Snapshot{
Timestamp: "20230302.052731",
BuildNumber: 9,
},
LastUpdated: "20230302052731",
SnapshotVersions: []maven.SnapshotVersion{
{
Extension: "jar",
Value: "3.3.1-20230302.052731-9",
Updated: "20230302052731",
},
{
Extension: "pom",
Value: "3.3.1-20230302.052731-9",
Updated: "20230302052731",
},
},
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("getVersionMetadata(%s, %s):\ngot %v\nwant %v\n", "org.example", "x.y.z", got, want)
}
}
41 changes: 0 additions & 41 deletions internal/resolution/datasource/maven_test.go

This file was deleted.

Loading