mirror of
https://github.com/safedep/vet.git
synced 2025-12-10 00:22:08 -06:00
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:
parent
c3d96dbef5
commit
06988f9b33
2
go.mod
2
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
169
pkg/readers/vsix_ext_reader.go
Normal file
169
pkg/readers/vsix_ext_reader.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
|
||||
4
scan.go
4
scan.go
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user