OpenVSX extensions scanning support (#536)

* feat(readers): Add OpenVSX ecosystem support

* refactor: use better naming conventions

* refactor: improve extensions reader with structured config
This commit is contained in:
Sahil Bansal 2025-07-15 18:40:02 +05:30 committed by GitHub
parent c3d96dbef5
commit 06988f9b33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 179 additions and 133 deletions

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.24.3
require (
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250610075857-7cfdb61a0bfa.2
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250610075857-7cfdb61a0bfa.1
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1
entgo.io/ent v0.14.4
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/BurntSushi/toml v1.5.0

2
go.sum
View File

@ -10,6 +10,8 @@ buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250610075857-7cfdb61a0bfa.2 h1:ENb
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20250610075857-7cfdb61a0bfa.2/go.mod h1:WDOWZglnweQ4njVEJpLYYpLMx9fD+e94KbKdt8oJrxY=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250610075857-7cfdb61a0bfa.1 h1:wOZtKj81Wq5fvHf4STR0vxEl8/peoEJkRzuQI+zwE2I=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250610075857-7cfdb61a0bfa.1/go.mod h1:uR95GqsnNCRn6cTyRBte6uMJMm0rEBRxTGpakKCNL9I=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1 h1:4sM5O5dx0yUucJ1trjZ8Cm9IGX2loEc4cUyh3Xy+5eU=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1/go.mod h1:uR95GqsnNCRn6cTyRBte6uMJMm0rEBRxTGpakKCNL9I=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=

View File

@ -35,6 +35,7 @@ const (
EcosystemTerraformModule = "TerraformModule"
EcosystemTerraformProvider = "TerraformProvider"
EcosystemVSCodeExtensions = "VSCodeExtensions"
EcosystemOpenVSXExtensions = "OpenVSXExtensions"
)
type ManifestSourceType string
@ -240,6 +241,8 @@ func (pm *PackageManifest) GetControlTowerSpecEcosystem() packagev1.Ecosystem {
return packagev1.Ecosystem_ECOSYSTEM_TERRAFORM_PROVIDER
case EcosystemVSCodeExtensions:
return packagev1.Ecosystem_ECOSYSTEM_VSCODE
case EcosystemOpenVSXExtensions:
return packagev1.Ecosystem_ECOSYSTEM_OPENVSX
default:
return packagev1.Ecosystem_ECOSYSTEM_UNSPECIFIED
}

View File

@ -1,128 +0,0 @@
package readers
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
)
const (
vsCodeExtensionExtensionsFileName = "extensions.json"
)
type vsCodeExtensionIdentifier struct {
Id string `json:"id"`
Uuid string `json:"uuid"`
}
type vsCodeExtensionLocation struct {
Path string `json:"path"`
Scheme string `json:"scheme"`
}
type vsCodeExtension struct {
Identifier vsCodeExtensionIdentifier `json:"identifier"`
Version string `json:"version"`
Location vsCodeExtensionLocation `json:"location"`
RelativeLocation string `json:"relativeLocation"`
}
type vsCodeExtensionList struct {
Extensions []vsCodeExtension
}
type vscodeExtReader struct {
distributionHomeDir map[string]string
}
var _ PackageManifestReader = (*vscodeExtReader)(nil)
func NewVSCodeExtReader(distributions []string) (*vscodeExtReader, error) {
customDistributions := make(map[string]string)
for i, distribution := range distributions {
customDistributions[fmt.Sprintf("custom-%d", i)] = distribution
}
return newVSCodeExtReaderFromDistributions(customDistributions)
}
func NewVSCodeExtReaderFromDefaultDistributions() (*vscodeExtReader, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
}
distributionHomeDir := map[string]string{
"code": filepath.Join(homeDir, ".vscode", "extensions"),
"cursor": filepath.Join(homeDir, ".cursor", "extensions"),
}
return &vscodeExtReader{distributionHomeDir: distributionHomeDir}, nil
}
func newVSCodeExtReaderFromDistributions(d map[string]string) (*vscodeExtReader, error) {
return &vscodeExtReader{distributionHomeDir: d}, nil
}
func (r *vscodeExtReader) Name() string {
return "VSCode Extensions Reader"
}
func (r *vscodeExtReader) ApplicationName() (string, error) {
return "installed-vscode-extensions", nil
}
func (r *vscodeExtReader) EnumManifests(handler func(*models.PackageManifest, PackageReader) error) error {
for distribution := range r.distributionHomeDir {
extensions, path, err := r.readExtensions(distribution)
if err != nil {
logger.Errorf("failed to read extensions for distribution %s: %v", distribution, err)
continue
}
manifest := models.NewPackageManifestFromLocal(path, models.EcosystemVSCodeExtensions)
for _, extension := range extensions.Extensions {
pkg := &models.Package{
PackageDetails: models.NewPackageDetail(models.EcosystemVSCodeExtensions, extension.Identifier.Id, extension.Version),
}
manifest.AddPackage(pkg)
}
err = handler(manifest, NewManifestModelReader(manifest))
if err != nil {
return err
}
}
return nil
}
func (r *vscodeExtReader) readExtensions(distribution string) (*vsCodeExtensionList, string, error) {
if _, ok := r.distributionHomeDir[distribution]; !ok {
return nil, "", fmt.Errorf("distribution %s not supported", distribution)
}
extensionsFile := filepath.Join(r.distributionHomeDir[distribution], vsCodeExtensionExtensionsFileName)
if _, err := os.Stat(extensionsFile); os.IsNotExist(err) {
return nil, "", fmt.Errorf("extensions file does not exist: %w", err)
}
file, err := os.Open(extensionsFile)
if err != nil {
return nil, "", fmt.Errorf("failed to open extensions file: %w", err)
}
defer file.Close()
var extensions vsCodeExtensionList
if err := json.NewDecoder(file).Decode(&extensions.Extensions); err != nil {
return nil, "", fmt.Errorf("failed to decode extensions file: %w", err)
}
return &extensions, extensionsFile, nil
}

View File

@ -0,0 +1,169 @@
package readers
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
)
const (
vsCodeExtensionExtensionsFileName = "extensions.json"
)
var editors = map[string]distributionInfo{
"code": {
FilePath: ".vscode/extensions",
Ecosystem: models.EcosystemVSCodeExtensions,
},
"vscodium": {
FilePath: ".vscode-oss/extensions",
Ecosystem: models.EcosystemOpenVSXExtensions,
},
"cursor": {
FilePath: ".cursor/extensions",
Ecosystem: models.EcosystemOpenVSXExtensions,
},
"windsurf": {
FilePath: ".windsurf/extensions",
Ecosystem: models.EcosystemOpenVSXExtensions,
},
}
type vsCodeExtensionIdentifier struct {
Id string `json:"id"`
Uuid string `json:"uuid"`
}
type vsCodeExtensionLocation struct {
Path string `json:"path"`
Scheme string `json:"scheme"`
}
type vsCodeExtension struct {
Identifier vsCodeExtensionIdentifier `json:"identifier"`
Version string `json:"version"`
Location vsCodeExtensionLocation `json:"location"`
RelativeLocation string `json:"relativeLocation"`
}
type vsCodeExtensionList struct {
Extensions []vsCodeExtension
}
type distributionInfo struct {
FilePath string // Path to the extensions directory
Ecosystem string // Type of extension marketplace (VSCode or OpenVSX)
}
type vsixExtReader struct {
distributions map[string]distributionInfo
}
var _ PackageManifestReader = (*vsixExtReader)(nil)
func NewVSIXExtReader(distributions []string) (*vsixExtReader, error) {
customDistributions := make(map[string]distributionInfo)
ecosystem := models.EcosystemVSCodeExtensions
for i, distribution := range distributions {
for _, eco := range editors {
if strings.Contains(distribution, eco.FilePath) {
ecosystem = eco.Ecosystem
break
}
}
customDistributions[fmt.Sprintf("custom-%d", i)] = distributionInfo{
FilePath: distribution,
Ecosystem: ecosystem,
}
}
return newVSCodeExtReaderFromDistributions(customDistributions)
}
func NewVSIXExtReaderFromDefaultDistributions() (*vsixExtReader, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
}
distributions := make(map[string]distributionInfo)
for editorName, config := range editors {
distributions[editorName] = distributionInfo{
FilePath: filepath.Join(homeDir, config.FilePath),
Ecosystem: config.Ecosystem,
}
}
return &vsixExtReader{distributions: distributions}, nil
}
func newVSCodeExtReaderFromDistributions(d map[string]distributionInfo) (*vsixExtReader, error) {
return &vsixExtReader{distributions: d}, nil
}
func (r *vsixExtReader) Name() string {
return "VSIX Extensions Reader"
}
func (r *vsixExtReader) ApplicationName() (string, error) {
return "installed-vsix-extensions", nil
}
func (r *vsixExtReader) EnumManifests(handler func(*models.PackageManifest, PackageReader) error) error {
for distribution := range r.distributions {
extensions, path, err := r.readExtensions(distribution)
if err != nil {
logger.Errorf("failed to read extensions for distribution %s: %v", distribution, err)
continue
}
info := r.distributions[distribution]
manifest := models.NewPackageManifestFromLocal(path, info.Ecosystem)
for _, extension := range extensions.Extensions {
pkg := &models.Package{
PackageDetails: models.NewPackageDetail(info.Ecosystem, extension.Identifier.Id, extension.Version),
}
manifest.AddPackage(pkg)
}
err = handler(manifest, NewManifestModelReader(manifest))
if err != nil {
return err
}
}
return nil
}
func (r *vsixExtReader) readExtensions(distribution string) (*vsCodeExtensionList, string, error) {
info, ok := r.distributions[distribution]
if !ok {
return nil, "", fmt.Errorf("distribution %s not supported", distribution)
}
extensionsFile := filepath.Join(info.FilePath, vsCodeExtensionExtensionsFileName)
if _, err := os.Stat(extensionsFile); os.IsNotExist(err) {
return nil, "", fmt.Errorf("extensions file does not exist: %w", err)
}
file, err := os.Open(extensionsFile)
if err != nil {
return nil, "", fmt.Errorf("failed to open extensions file: %w", err)
}
defer file.Close()
var extensions vsCodeExtensionList
if err := json.NewDecoder(file).Decode(&extensions.Extensions); err != nil {
return nil, "", fmt.Errorf("failed to decode extensions file: %w", err)
}
return &extensions, extensionsFile, nil
}

View File

@ -8,13 +8,13 @@ import (
)
func TestVSCodeExtReaderInit(t *testing.T) {
reader, err := NewVSCodeExtReaderFromDefaultDistributions()
reader, err := NewVSIXExtReaderFromDefaultDistributions()
assert.NoError(t, err)
assert.NotNil(t, reader)
}
func TestVSCodeExtReaderEnumManifests(t *testing.T) {
reader, err := NewVSCodeExtReader([]string{"./fixtures/vsx"})
reader, err := NewVSIXExtReader([]string{"./fixtures/vsx"})
assert.NoError(t, err)
assert.NotNil(t, reader)

View File

@ -396,12 +396,12 @@ func internalStartScan() error {
analytics.TrackCommandScanVSCodeExtScan()
// nolint:ineffassign,staticcheck
reader, err = readers.NewVSCodeExtReaderFromDefaultDistributions()
reader, err = readers.NewVSIXExtReaderFromDefaultDistributions()
} else {
analytics.TrackCommandScanVSCodeExtScan()
// nolint:ineffassign,staticcheck
reader, err = readers.NewVSCodeExtReader(vsxDirectories)
reader, err = readers.NewVSIXExtReader(vsxDirectories)
}
} else if len(scanImageTarget) != 0 {
analytics.TrackCommandImageScan()