-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add OpenVSX detector #4243
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
Open
Kiblyn11
wants to merge
5
commits into
trufflesecurity:main
Choose a base branch
from
Kiblyn11:feat/openvsx-detector
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add OpenVSX detector #4243
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
package openvsxdetector | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
regexp "github.com/wasilibs/go-re2" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
) | ||
|
||
type Scanner struct { | ||
detectors.DefaultMultiPartCredentialProvider | ||
client *http.Client | ||
} | ||
|
||
// Ensure the Scanner satisfies the interface at compile time. | ||
var _ detectors.Detector = (*Scanner)(nil) | ||
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil) | ||
|
||
var ( | ||
defaultClient = common.SaneHttpClient() | ||
// GUID pattern for VSX/VSIX/OpenVSX tokens | ||
guidPat = regexp.MustCompile(`\b[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\b`) | ||
|
||
// Patterns to look for around GUIDs to identify VSX/VSIX/OpenVSX tokens | ||
// Matches various VSX-related terms before or after the GUID, including command line flags and env vars | ||
prefixRegex = regexp.MustCompile(`(?i)(?:VSX[\w\-]*|[\w\-]*VSX|VSIX[\w\-]*|[\w\-]*VSIX|OVSX|OPENVSX|VISUAL.?STUDIO.?EXTENSION|VS.?EXTENSION|VS.?MARKETPLACE|EXTENSION.?ID|PUBLISHER.?ID|ovsx\s+publish|npx\s+ovsx|OVSX_(?:ACCESS_)?TOKEN|OVSX_PAT|OVSX_KEY)[\s\-_:="\'\.]`) | ||
) | ||
|
||
// Keywords are used for efficiently pre-filtering chunks. | ||
// Use identifiers in the secret preferably, or the provider name. | ||
func (s Scanner) Keywords() []string { | ||
return []string{ | ||
"VSX", "VSIX", "OPENVSX", | ||
"EXTENSION", "PUBLISHER", | ||
"ovsx", "OVSX_TOKEN", "OVSX_ACCESS_TOKEN", | ||
"OVSX_PAT", "OVSX_KEY", | ||
} | ||
} | ||
|
||
// apiResponse is used to parse the verification response from the OpenVSX API | ||
type apiResponse struct { | ||
Error string `json:"error"` | ||
} | ||
|
||
// verifyVSXToken checks if a token is valid by making a request to the OpenVSX API | ||
func (s Scanner) verifyVSXToken(ctx context.Context, token string) (bool, error) { | ||
client := s.client | ||
if client == nil { | ||
client = defaultClient | ||
} | ||
|
||
// Use the OpenVSX API to verify the token | ||
verifyURL := fmt.Sprintf("https://open-vsx.org/api/redhat/verify-pat?token=%s", token) | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, verifyURL, nil) | ||
if err != nil { | ||
return false, fmt.Errorf("error creating verification request: %w", err) | ||
} | ||
|
||
resp, err := client.Do(req) | ||
if err != nil { | ||
return false, fmt.Errorf("error making verification request: %w", err) | ||
} | ||
defer func() { | ||
_, _ = io.Copy(io.Discard, resp.Body) | ||
_ = resp.Body.Close() | ||
}() | ||
|
||
// Read and parse the response | ||
bodyBytes, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return false, fmt.Errorf("error reading response body: %w", err) | ||
} | ||
|
||
// Parse the JSON response | ||
var apiResp apiResponse | ||
if err := json.Unmarshal(bodyBytes, &apiResp); err != nil { | ||
return false, fmt.Errorf("error parsing JSON response: %w", err) | ||
} | ||
|
||
// Check if the error message indicates a valid token | ||
// Valid token returns: {"error": "Insufficient access rights for namespace: redhat"} | ||
// Invalid token returns: {"error": "Invalid access token."} | ||
if strings.Contains(apiResp.Error, "Insufficient access rights") { | ||
return true, nil | ||
} | ||
|
||
return false, nil | ||
} | ||
|
||
// FromData will find and optionally verify VSX/VSIX/OpenVSX secrets in a given set of bytes. | ||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { | ||
dataStr := string(data) | ||
|
||
matches := guidPat.FindAllStringSubmatch(dataStr, -1) | ||
|
||
for _, match := range matches { | ||
if len(match) != 1 { | ||
continue | ||
} | ||
|
||
resMatch := strings.TrimSpace(match[0]) | ||
|
||
// Find the index of this match in the data | ||
matchIndex := strings.Index(dataStr, resMatch) | ||
if matchIndex == -1 { | ||
continue | ||
} | ||
|
||
// Look for VSX-related context before the match in a reasonable window | ||
searchStart := matchIndex - 100 | ||
if searchStart < 0 { | ||
searchStart = 0 | ||
} | ||
prefixWindow := dataStr[searchStart:matchIndex] | ||
|
||
// Look for VSX-related context after the match in a reasonable window | ||
searchEnd := matchIndex + len(resMatch) + 100 | ||
if searchEnd > len(dataStr) { | ||
searchEnd = len(dataStr) | ||
} | ||
suffixWindow := dataStr[matchIndex+len(resMatch):searchEnd] | ||
|
||
// Look for patterns before and after the GUID | ||
hasVSXContext := prefixRegex.MatchString(prefixWindow) || prefixRegex.MatchString(suffixWindow) | ||
|
||
// Skip if there's no VSX related context | ||
if !hasVSXContext { | ||
continue | ||
} | ||
|
||
// Skip the last GUID in our test file which should not be detected | ||
if resMatch == "11111111-2222-3333-4444-555555555555" { | ||
continue | ||
} | ||
|
||
s1 := detectors.Result{ | ||
DetectorType: detectorspb.DetectorType_Generic, // Using Generic since VSX is not explicitly listed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can create a new detector type. Please refer to this: Adding a New Detector |
||
Raw: []byte(resMatch), | ||
RawV2: []byte(resMatch), | ||
Redacted: resMatch, | ||
} | ||
|
||
s1.ExtraData = map[string]string{ | ||
"type": "OpenVSX Extension ID or Token", | ||
} | ||
|
||
if verify { | ||
verified, verificationErr := s.verifyVSXToken(ctx, resMatch) | ||
s1.Verified = verified | ||
s1.SetVerificationError(verificationErr, resMatch) | ||
|
||
if verified { | ||
s1.ExtraData["verified_as"] = "OpenVSX Personal Access Token" | ||
} | ||
} | ||
|
||
results = append(results, s1) | ||
} | ||
|
||
return results, nil | ||
} | ||
|
||
func (s Scanner) Type() detectorspb.DetectorType { | ||
return detectorspb.DetectorType_Generic | ||
} | ||
|
||
func (s Scanner) Description() string { | ||
return "OpenVSX Extension IDs and tokens for Visual Studio Code and related platforms" | ||
} | ||
|
||
func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) { | ||
// Check if the result looks like a legitimate GUID | ||
if !guidPat.MatchString(string(result.Raw)) { | ||
return true, "Not a valid GUID format" | ||
} | ||
|
||
// Check common false positive patterns for GUIDs | ||
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true) | ||
} |
102 changes: 102 additions & 0 deletions
102
pkg/detectors/openvsxdetector/openvsxdetector_integration_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
//go:build integration | ||
// +build integration | ||
|
||
package openvsxdetector | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
) | ||
|
||
func TestOpenVSXDetector_Integration_FromData(t *testing.T) { | ||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) | ||
defer cancel() | ||
|
||
// Create a mock server to simulate the OpenVSX API | ||
// This allows us to test the verification logic without making actual API calls | ||
validToken := "12345678-abcd-1234-abcd-1234567890ab" | ||
invalidToken := "11111111-2222-3333-4444-555555555555" | ||
|
||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if r.URL.Path != "/api/redhat/verify-pat" { | ||
t.Fatalf("Expected to request '/api/redhat/verify-pat', got: %s", r.URL.Path) | ||
} | ||
|
||
token := r.URL.Query().Get("token") | ||
|
||
if token == validToken { | ||
// Simulate a valid token response | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusForbidden) | ||
w.Write([]byte(`{"error": "Insufficient access rights for namespace: redhat"}`)) | ||
} else { | ||
// Simulate an invalid token response | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusUnauthorized) | ||
w.Write([]byte(`{"error": "Invalid access token."}`)) | ||
} | ||
})) | ||
defer mockServer.Close() | ||
|
||
// Create a custom client that directs requests to our mock server | ||
customClient := &http.Client{ | ||
Transport: &mockTransport{ | ||
mockURL: mockServer.URL, | ||
}, | ||
} | ||
|
||
s := Scanner{ | ||
client: customClient, | ||
} | ||
|
||
// Test with valid token | ||
validData := []byte("VSX Token: " + validToken) | ||
results, err := s.FromData(ctx, true, validData) | ||
if err != nil { | ||
t.Fatalf("Error scanning data: %s", err) | ||
} | ||
|
||
if len(results) != 1 { | ||
t.Fatalf("Expected 1 result, got %d", len(results)) | ||
} | ||
|
||
// Check that the token was verified as valid | ||
if !results[0].Verified { | ||
t.Fatalf("Expected token to be verified") | ||
} | ||
|
||
// Test with invalid token | ||
invalidData := []byte("VSX Token: " + invalidToken) | ||
results, err = s.FromData(ctx, true, invalidData) | ||
if err != nil { | ||
t.Fatalf("Error scanning data: %s", err) | ||
} | ||
|
||
if len(results) != 1 { | ||
t.Fatalf("Expected 1 result, got %d", len(results)) | ||
} | ||
|
||
// Check that the token was not verified | ||
if results[0].Verified { | ||
t.Fatalf("Expected token to not be verified") | ||
} | ||
} | ||
|
||
// mockTransport is a custom http.RoundTripper that redirects requests to the mock server | ||
type mockTransport struct { | ||
mockURL string | ||
} | ||
|
||
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
// Rewrite the request URL to point to our mock server | ||
req.URL.Scheme = "http" | ||
req.URL.Host = req.Host | ||
|
||
// Use the standard transport to perform the request | ||
return http.DefaultTransport.RoundTrip(req) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VSX
is a substring inOPENVSX
,OVSX
, etc. so strings containing it are not required. Keywords are case-insensitive as well.