Skip to content

Commit 6861df8

Browse files
authored
Merge branch 'main' into feat/async-update-checker
2 parents f19d1d0 + 7517804 commit 6861df8

24 files changed

Lines changed: 428 additions & 205 deletions

docs/reporting.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ vet query --from ./scan-data \
109109
Run `vet scan -h` to see the complete list of reporters and options:
110110

111111
- `--report-summary` - Console summary (default)
112-
- `--report-console` - Interactive table output
113112
- `--report-sarif` - SARIF format
114113
- `--report-json` - JSON format
115114
- `--report-markdown` - Detailed markdown

mcp/tools/common.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,22 @@ package tools
33
import (
44
"encoding/json"
55
"fmt"
6+
7+
mcpgo "github.com/mark3labs/mcp-go/mcp"
8+
)
9+
10+
const (
11+
llmResponseTypeError = "ERROR"
12+
llmErrorCodePackageInsightNotFound = "PACKAGE_INSIGHT_NOT_FOUND"
13+
llmErrorCodeUpstreamError = "UPSTREAM_ERROR"
614
)
715

16+
type llmErrorResponse struct {
17+
Type string `json:"type"`
18+
Message string `json:"message"`
19+
Code string `json:"code,omitempty"`
20+
}
21+
822
func serializeForLlm(msg any) (string, error) {
923
json, err := json.Marshal(msg)
1024
if err != nil {
@@ -13,3 +27,20 @@ func serializeForLlm(msg any) (string, error) {
1327

1428
return fmt.Sprintf("```json\n%s\n```", string(json)), nil
1529
}
30+
31+
func serializeErrorForLlm(message, code string) (string, error) {
32+
return serializeForLlm(llmErrorResponse{
33+
Type: llmResponseTypeError,
34+
Message: message,
35+
Code: code,
36+
})
37+
}
38+
39+
func toolResultFromLlmError(message, code string) (*mcpgo.CallToolResult, error) {
40+
serialized, err := serializeErrorForLlm(message, code)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to serialize error response: %w", err)
43+
}
44+
45+
return mcpgo.NewToolResultText(serialized), nil
46+
}

mcp/tools/common_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
78
)
89

910
func TestSerializeForLlm(t *testing.T) {
@@ -68,3 +69,21 @@ func TestSerializeForLlm(t *testing.T) {
6869
})
6970
}
7071
}
72+
73+
func TestSerializeErrorForLlm(t *testing.T) {
74+
result, err := serializeErrorForLlm("package insights unavailable", llmErrorCodePackageInsightNotFound)
75+
76+
assert.NoError(t, err)
77+
assert.Contains(t, result, `"type":"ERROR"`)
78+
assert.Contains(t, result, "package insights unavailable")
79+
assert.Contains(t, result, llmErrorCodePackageInsightNotFound)
80+
}
81+
82+
func TestToolResultFromLlmError(t *testing.T) {
83+
result, err := toolResultFromLlmError("upstream failure", llmErrorCodeUpstreamError)
84+
85+
assert.NoError(t, err)
86+
require.NotNil(t, result)
87+
assert.False(t, result.IsError)
88+
assert.Len(t, result.Content, 1)
89+
}

mcp/tools/package_insights.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tools
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
mcpgo "github.com/mark3labs/mcp-go/mcp"
@@ -67,7 +68,17 @@ func (t *packageInsightsTool) executeGetPackageVulnerabilities(ctx context.Conte
6768

6869
vulns, err := t.driver.GetPackageVersionVulnerabilities(ctx, parsedPurl.PackageVersion())
6970
if err != nil {
70-
return nil, fmt.Errorf("failed to get package vulnerabilities: %w", err)
71+
if errors.Is(err, mcp.ErrPackageVersionInsightNotFound) {
72+
return toolResultFromLlmError(
73+
fmt.Sprintf("no package insights found for package: %s", purl),
74+
llmErrorCodePackageInsightNotFound,
75+
)
76+
}
77+
78+
return toolResultFromLlmError(
79+
fmt.Sprintf("failed to get package vulnerabilities: %v", err),
80+
llmErrorCodeUpstreamError,
81+
)
7182
}
7283

7384
logger.Debugf("Found %d vulnerabilities for package: %s", len(vulns), purl)
@@ -97,7 +108,17 @@ func (t *packageInsightsTool) executeGetPackagePopularity(ctx context.Context,
97108

98109
popularity, err := t.driver.GetPackageVersionPopularity(ctx, parsedPurl.PackageVersion())
99110
if err != nil {
100-
return nil, fmt.Errorf("failed to get package popularity: %w", err)
111+
if errors.Is(err, mcp.ErrPackageVersionInsightNotFound) {
112+
return toolResultFromLlmError(
113+
fmt.Sprintf("no package insights found for package: %s", purl),
114+
llmErrorCodePackageInsightNotFound,
115+
)
116+
}
117+
118+
return toolResultFromLlmError(
119+
fmt.Sprintf("failed to get package popularity: %v", err),
120+
llmErrorCodeUpstreamError,
121+
)
101122
}
102123

103124
logger.Debugf("Found %d popularity for package: %s", len(popularity), purl)
@@ -127,11 +148,24 @@ func (t *packageInsightsTool) executeGetPackageLicenseInfo(ctx context.Context,
127148

128149
licenseInfo, err := t.driver.GetPackageVersionLicenseInfo(ctx, parsedPurl.PackageVersion())
129150
if err != nil {
130-
return nil, fmt.Errorf("failed to get package license info: %w", err)
151+
if errors.Is(err, mcp.ErrPackageVersionInsightNotFound) {
152+
return toolResultFromLlmError(
153+
fmt.Sprintf("no package insights found for package: %s", purl),
154+
llmErrorCodePackageInsightNotFound,
155+
)
156+
}
157+
158+
return toolResultFromLlmError(
159+
fmt.Sprintf("failed to get package license info: %v", err),
160+
llmErrorCodeUpstreamError,
161+
)
131162
}
132163

133164
if licenseInfo == nil {
134-
return nil, fmt.Errorf("no license info returned for package: %s", purl)
165+
return toolResultFromLlmError(
166+
fmt.Sprintf("no license info returned for package: %s", purl),
167+
llmErrorCodePackageInsightNotFound,
168+
)
135169
}
136170

137171
logger.Debugf("Found %d license info for package: %s", len(licenseInfo.Licenses), purl)

mcp/tools/package_insights_test.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tools
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"testing"
78

89
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
@@ -72,8 +73,8 @@ func TestPackageInsightsTool_ExecuteGetPackageVulnerabilities(t *testing.T) {
7273
driver.On("GetPackageVersionVulnerabilities", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
7374
Return(([]*vulnerabilityv1.Vulnerability)(nil), mcp.ErrPackageVersionInsightNotFound)
7475
},
75-
expectedContains: "",
76-
expectedError: "failed to get package vulnerabilities",
76+
expectedContains: "PACKAGE_INSIGHT_NOT_FOUND",
77+
expectedError: "",
7778
},
7879
{
7980
name: "driver returns other error",
@@ -84,8 +85,8 @@ func TestPackageInsightsTool_ExecuteGetPackageVulnerabilities(t *testing.T) {
8485
driver.On("GetPackageVersionVulnerabilities", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
8586
Return(([]*vulnerabilityv1.Vulnerability)(nil), errors.New("internal server error"))
8687
},
87-
expectedContains: "",
88-
expectedError: "failed to get package vulnerabilities",
88+
expectedContains: "UPSTREAM_ERROR",
89+
expectedError: "",
8990
},
9091
}
9192

@@ -180,6 +181,18 @@ func TestPackageInsightsTool_ExecuteGetPackagePopularity(t *testing.T) {
180181
expectedContains: "",
181182
expectedError: "invalid purl",
182183
},
184+
{
185+
name: "driver returns wrapped insight not found error",
186+
requestArgs: map[string]interface{}{
187+
"purl": "pkg:npm/express@4.17.1",
188+
},
189+
setupDriver: func(driver *MockDriver) {
190+
driver.On("GetPackageVersionPopularity", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
191+
Return(([]*packagev1.ProjectInsight)(nil), fmt.Errorf("failed to get package version insight: %w", mcp.ErrPackageVersionInsightNotFound))
192+
},
193+
expectedContains: "PACKAGE_INSIGHT_NOT_FOUND",
194+
expectedError: "",
195+
},
183196
{
184197
name: "driver returns error",
185198
requestArgs: map[string]interface{}{
@@ -189,8 +202,8 @@ func TestPackageInsightsTool_ExecuteGetPackagePopularity(t *testing.T) {
189202
driver.On("GetPackageVersionPopularity", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
190203
Return(([]*packagev1.ProjectInsight)(nil), errors.New("internal server error"))
191204
},
192-
expectedContains: "",
193-
expectedError: "failed to get package popularity",
205+
expectedContains: "UPSTREAM_ERROR",
206+
expectedError: "",
194207
},
195208
}
196209

@@ -292,8 +305,8 @@ func TestPackageInsightsTool_ExecuteGetPackageLicenseInfo(t *testing.T) {
292305
driver.On("GetPackageVersionLicenseInfo", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
293306
Return((*packagev1.LicenseMetaList)(nil), errors.New("internal server error"))
294307
},
295-
expectedContains: "",
296-
expectedError: "failed to get package license info",
308+
expectedContains: "UPSTREAM_ERROR",
309+
expectedError: "",
297310
},
298311

299312
{
@@ -305,8 +318,8 @@ func TestPackageInsightsTool_ExecuteGetPackageLicenseInfo(t *testing.T) {
305318
driver.On("GetPackageVersionLicenseInfo", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
306319
Return(nil, nil)
307320
},
308-
expectedContains: "",
309-
expectedError: "no license info returned for package",
321+
expectedContains: "PACKAGE_INSIGHT_NOT_FOUND",
322+
expectedError: "",
310323
},
311324
}
312325

mcp/tools/package_malware.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tools
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
mcpgo "github.com/mark3labs/mcp-go/mcp"
@@ -52,12 +53,14 @@ func (t *packageMalwareTool) executeCheckPackageMalware(ctx context.Context,
5253

5354
malwareReport, err := t.driver.GetPackageVersionMalwareReport(ctx, parsedPurl.PackageVersion())
5455
if err != nil {
55-
// Handle this error gracefully otherwise LLMs will hallucinate
56-
if err == mcp.ErrMaliciousPackageScanningPackageNotFound {
56+
if errors.Is(err, mcp.ErrMaliciousPackageScanningPackageNotFound) {
5757
return mcpgo.NewToolResultText("No known malware found for this package"), nil
5858
}
5959

60-
return nil, fmt.Errorf("failed to get package version malware report: %w", err)
60+
return toolResultFromLlmError(
61+
fmt.Sprintf("failed to get package version malware report: %v", err),
62+
llmErrorCodeUpstreamError,
63+
)
6164
}
6265

6366
malwareReportJson, err := serializeForLlm(malwareReport)

mcp/tools/package_malware_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tools
22

33
import (
44
"context"
5+
"errors"
56
"testing"
67

78
malysisv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
@@ -72,6 +73,18 @@ func TestPackageMalwareTool_ExecuteCheckPackageMalware(t *testing.T) {
7273
expectedContains: "No known malware found for this package",
7374
expectedError: "",
7475
},
76+
{
77+
name: "driver returns upstream error",
78+
requestArgs: map[string]interface{}{
79+
"purl": "pkg:npm/express@4.17.1",
80+
},
81+
setupDriver: func(driver *MockDriver) {
82+
driver.On("GetPackageVersionMalwareReport", mock.Anything, mock.AnythingOfType("*packagev1.PackageVersion")).
83+
Return((*malysisv1pb.Report)(nil), errors.New("upstream unavailable"))
84+
},
85+
expectedContains: "UPSTREAM_ERROR",
86+
expectedError: "",
87+
},
7588
}
7689

7790
for _, tt := range tests {

mcp/tools/package_registry.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ func (t *packageRegistryTool) executeGetPackageLatestVersion(ctx context.Context
5858

5959
latestVersion, err := t.driver.GetPackageLatestVersion(ctx, parsedPurl.PackageVersion().GetPackage())
6060
if err != nil {
61-
return nil, fmt.Errorf("failed to get package latest version: %w", err)
61+
return toolResultFromLlmError(
62+
fmt.Sprintf("failed to get package latest version: %v", err),
63+
llmErrorCodeUpstreamError,
64+
)
6265
}
6366

6467
latestVersionJson, err := serializeForLlm(latestVersion)
@@ -86,7 +89,10 @@ func (t *packageRegistryTool) executeGetPackageAvailableVersions(ctx context.Con
8689

8790
availableVersions, err := t.driver.GetPackageAvailableVersions(ctx, parsedPurl.PackageVersion().GetPackage())
8891
if err != nil {
89-
return nil, fmt.Errorf("failed to get package available versions: %w", err)
92+
return toolResultFromLlmError(
93+
fmt.Sprintf("failed to get package available versions: %v", err),
94+
llmErrorCodeUpstreamError,
95+
)
9096
}
9197

9298
availableVersionsJson, err := serializeForLlm(availableVersions)

mcp/tools/package_registry_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ func TestPackageRegistryTool_ExecuteGetPackageLatestVersion(t *testing.T) {
6868
driver.On("GetPackageLatestVersion", mock.Anything, mock.AnythingOfType("*packagev1.Package")).
6969
Return((*packagev1.PackageVersion)(nil), errors.New("registry unavailable"))
7070
},
71-
expectedContains: "",
72-
expectedError: "failed to get package latest version",
71+
expectedContains: "UPSTREAM_ERROR",
72+
expectedError: "",
7373
},
7474
}
7575

@@ -179,8 +179,8 @@ func TestPackageRegistryTool_ExecuteGetPackageAvailableVersions(t *testing.T) {
179179
driver.On("GetPackageAvailableVersions", mock.Anything, mock.AnythingOfType("*packagev1.Package")).
180180
Return(([]*packagev1.PackageVersion)(nil), errors.New("registry unavailable"))
181181
},
182-
expectedContains: "",
183-
expectedError: "failed to get package available versions",
182+
expectedContains: "UPSTREAM_ERROR",
183+
expectedError: "",
184184
},
185185
}
186186

pkg/common/purl/purl.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ func purlBuildLockfilePackageName(ecosystem lockfile.Ecosystem, group, name stri
6565

6666
func PurlTypeToEcosystem(purlType string) (lockfile.Ecosystem, error) {
6767
knownTypes := map[string]lockfile.Ecosystem{
68-
packageurl.TypeCargo: lockfile.CargoEcosystem,
68+
// osv-scanner's lockfile.CargoEcosystem is "crates.io" which does not
69+
// match vet's canonical model ecosystem ("Cargo"). Returning the raw
70+
// lockfile value makes GetControlTowerSpecEcosystem() resolve to
71+
// ECOSYSTEM_UNSPECIFIED, silently disabling malware/insights enrichment
72+
// for cargo PURLs. Map to the model ecosystem like we do for the
73+
// GitHub Actions / VSCode / OpenVSX types below.
74+
packageurl.TypeCargo: lockfile.Ecosystem(models.EcosystemCargo),
6975
packageurl.TypeComposer: lockfile.ComposerEcosystem,
7076
packageurl.TypeGolang: lockfile.GoEcosystem,
7177
packageurl.TypeMaven: lockfile.MavenEcosystem,

0 commit comments

Comments
 (0)