mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Support for scanning manifest files (#36)
* feat: add extractor logic for manifest files * feat: add manifest-based package installation detection * feat: add osv-scanner for extracting lockfiles * refactor: add comment for cmd parse * refactor: add Ecosystem() method to PackageManager interface * refactor: implement package-manager-specific extractors & replace osv-scanner with scalibr * Update extractor/extractor.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> --------- Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,6 @@ package npm
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
)
|
||||
|
||||
// PackageManagerExtractor defines the interface for package-manager-specific extractors
|
||||
type PackageManagerExtractor interface {
|
||||
// Returns the list of supported lockfiles by the package manager
|
||||
GetSupportedFiles() []string
|
||||
|
||||
// Returns the package manager ecosysetm
|
||||
GetEcosystem() packagev1.Ecosystem
|
||||
|
||||
// Returns the package manager name
|
||||
GetPackageManager() PackageManagerName
|
||||
|
||||
// Extracts the packages from the lockfile
|
||||
Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error)
|
||||
}
|
||||
|
||||
type PackageManagerName string
|
||||
|
||||
const (
|
||||
Npm PackageManagerName = "npm"
|
||||
Pnpm PackageManagerName = "pnpm"
|
||||
Pip PackageManagerName = "pip"
|
||||
)
|
||||
|
||||
type ExtractorManager struct {
|
||||
extractors map[PackageManagerName]PackageManagerExtractor
|
||||
}
|
||||
|
||||
func NewExtractorManager() *ExtractorManager {
|
||||
return &ExtractorManager{
|
||||
extractors: map[PackageManagerName]PackageManagerExtractor{
|
||||
Npm: &NpmExtractor{},
|
||||
Pnpm: &PnpmExtractor{},
|
||||
Pip: &PipExtractor{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExtractorManager) GetExtractorForPackageManager(pmn PackageManagerName) PackageManagerExtractor {
|
||||
return e.extractors[pmn]
|
||||
}
|
||||
|
||||
func (e *ExtractorManager) GetSupportedFilesForPackageManager(pmn PackageManagerName) []string {
|
||||
return e.extractors[pmn].GetSupportedFiles()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/dry/log"
|
||||
)
|
||||
|
||||
type ExtractorConfig struct {
|
||||
ExtractorPackageManager PackageManagerName
|
||||
ScanDir string
|
||||
}
|
||||
|
||||
type extractor struct {
|
||||
Config ExtractorConfig
|
||||
extractorManager ExtractorManager
|
||||
}
|
||||
|
||||
func NewDefaultExtractorConfig() *ExtractorConfig {
|
||||
return &ExtractorConfig{
|
||||
ScanDir: ".",
|
||||
}
|
||||
}
|
||||
|
||||
func New(config ExtractorConfig) *extractor {
|
||||
return &extractor{
|
||||
Config: config,
|
||||
extractorManager: *NewExtractorManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *extractor) ExtractManifest() ([]*packagev1.PackageVersion, error) {
|
||||
packagesToAnalyze := []*packagev1.PackageVersion{}
|
||||
|
||||
// Get the list of lockfiles to check based on ecosystem
|
||||
filesToCheck := e.getFilesToCheck()
|
||||
|
||||
for _, filename := range filesToCheck {
|
||||
filePath := filepath.Join(e.Config.ScanDir, filename)
|
||||
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
extractor, err := e.getExtractorForFile()
|
||||
if err != nil {
|
||||
log.Warnf("failed to get extractor\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract packages from this lockfile
|
||||
packages, err := extractor.Extract(filePath, e.Config.ScanDir)
|
||||
if err != nil {
|
||||
log.Warnf("failed to extract from %s: %v\n", filePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
packagesToAnalyze = append(packagesToAnalyze, packages...)
|
||||
}
|
||||
|
||||
return packagesToAnalyze, nil
|
||||
}
|
||||
|
||||
func (e *extractor) getFilesToCheck() []string {
|
||||
return e.extractorManager.GetSupportedFilesForPackageManager(e.Config.ExtractorPackageManager)
|
||||
}
|
||||
|
||||
func (e *extractor) getExtractorForFile() (PackageManagerExtractor, error) {
|
||||
extractor := e.extractorManager.GetExtractorForPackageManager(e.Config.ExtractorPackageManager)
|
||||
if extractor == nil {
|
||||
return nil, fmt.Errorf("no extractor found for the specified package manager: %s", e.Config.ExtractorPackageManager)
|
||||
}
|
||||
return extractor, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/google/osv-scalibr/extractor/filesystem"
|
||||
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson"
|
||||
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/pnpmlock"
|
||||
"github.com/google/osv-scalibr/fs"
|
||||
)
|
||||
|
||||
// NpmExtractor handles package-lock.json files
|
||||
type NpmExtractor struct{}
|
||||
|
||||
func (n *NpmExtractor) GetSupportedFiles() []string {
|
||||
return []string{"package-lock.json"}
|
||||
}
|
||||
|
||||
func (n *NpmExtractor) GetEcosystem() packagev1.Ecosystem {
|
||||
return packagev1.Ecosystem_ECOSYSTEM_NPM
|
||||
}
|
||||
|
||||
func (n *NpmExtractor) GetPackageManager() PackageManagerName {
|
||||
return Npm
|
||||
}
|
||||
|
||||
func (n *NpmExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
|
||||
return parseNpmPackageLockFile(lockfilePath, scanDir)
|
||||
}
|
||||
|
||||
func parseNpmPackageLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
|
||||
packagelockExtractor := packagelockjson.NewDefault()
|
||||
|
||||
file, err := os.Open(lockfilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open lockfile: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
inputConfig := &filesystem.ScanInput{
|
||||
FS: fs.DirFS(scanDir),
|
||||
Path: lockfilePath,
|
||||
Reader: file,
|
||||
}
|
||||
|
||||
inventory, err := packagelockExtractor.Extract(context.Background(), inputConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract packages: %s", err)
|
||||
}
|
||||
|
||||
var packages []*packagev1.PackageVersion
|
||||
|
||||
for _, invPkg := range inventory.Packages {
|
||||
pkg := &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Name: invPkg.Name,
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
},
|
||||
Version: invPkg.Version,
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// PnpmExtractor handles pnpm-lock.yaml files
|
||||
type PnpmExtractor struct{}
|
||||
|
||||
func (p *PnpmExtractor) GetSupportedFiles() []string {
|
||||
return []string{"pnpm-lock.yaml"}
|
||||
}
|
||||
|
||||
func (p *PnpmExtractor) GetEcosystem() packagev1.Ecosystem {
|
||||
return packagev1.Ecosystem_ECOSYSTEM_NPM
|
||||
}
|
||||
|
||||
func (p *PnpmExtractor) GetPackageManager() PackageManagerName {
|
||||
return Pnpm
|
||||
}
|
||||
|
||||
func (p *PnpmExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
|
||||
return parsePnpmLockFile(lockfilePath, scanDir)
|
||||
}
|
||||
|
||||
func parsePnpmLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
|
||||
pnpmLockExtractor := pnpmlock.New()
|
||||
|
||||
file, err := os.Open(lockfilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open lockfile: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
inputConfig := &filesystem.ScanInput{
|
||||
FS: fs.DirFS(scanDir),
|
||||
Path: lockfilePath,
|
||||
Reader: file,
|
||||
}
|
||||
|
||||
inventory, err := pnpmLockExtractor.Extract(context.Background(), inputConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract packages: %s", err)
|
||||
}
|
||||
|
||||
var packages []*packagev1.PackageVersion
|
||||
|
||||
for _, invPkg := range inventory.Packages {
|
||||
pkg := &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Name: invPkg.Name,
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
},
|
||||
Version: invPkg.Version,
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/google/osv-scalibr/extractor/filesystem"
|
||||
"github.com/google/osv-scalibr/extractor/filesystem/language/python/requirements"
|
||||
"github.com/google/osv-scalibr/fs"
|
||||
)
|
||||
|
||||
// PipExtractor handles requirements.txt files
|
||||
type PipExtractor struct{}
|
||||
|
||||
func (p *PipExtractor) GetSupportedFiles() []string {
|
||||
return []string{"requirements.txt"}
|
||||
}
|
||||
|
||||
func (p *PipExtractor) GetEcosystem() packagev1.Ecosystem {
|
||||
return packagev1.Ecosystem_ECOSYSTEM_PYPI
|
||||
}
|
||||
|
||||
func (p *PipExtractor) GetPackageManager() PackageManagerName {
|
||||
return Pip
|
||||
}
|
||||
|
||||
func (n *PipExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
|
||||
return parseRequirementsTxtFile(lockfilePath, scanDir)
|
||||
}
|
||||
|
||||
func parseRequirementsTxtFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
|
||||
requirementsExtractor := requirements.NewDefault()
|
||||
|
||||
file, err := os.Open(lockfilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open lockfile: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
inputConfig := &filesystem.ScanInput{
|
||||
FS: fs.DirFS(scanDir),
|
||||
Path: lockfilePath,
|
||||
Reader: file,
|
||||
}
|
||||
|
||||
inventory, err := requirementsExtractor.Extract(context.Background(), inputConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract packages: %s", err)
|
||||
}
|
||||
|
||||
var packages []*packagev1.PackageVersion
|
||||
|
||||
for _, invPkg := range inventory.Packages {
|
||||
pkg := &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Name: invPkg.Name,
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
|
||||
},
|
||||
Version: invPkg.Version,
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
@@ -9,11 +9,12 @@ require (
|
||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250418165058-162f6b0cc319.1
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/google/osv-scalibr v0.2.1
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.7
|
||||
github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
google.golang.org/grpc v1.72.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -64,6 +65,9 @@ require (
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.9 // indirect
|
||||
github.com/go-critic/go-critic v0.12.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.14.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
@@ -100,6 +104,7 @@ require (
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jgautheron/goconst v1.7.1 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.4 // indirect
|
||||
@@ -133,6 +138,7 @@ require (
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/package-url/packageurl-go v0.1.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/polyfloyd/go-errorlint v1.7.1 // indirect
|
||||
@@ -207,6 +213,7 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
|
||||
@@ -119,6 +119,12 @@ github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYF
|
||||
github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
|
||||
github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
|
||||
github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
|
||||
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -194,6 +200,8 @@ github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHS
|
||||
github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/osv-scalibr v0.2.1 h1:d1SpwzXfNiRafUMNpei3tQg8dDLSWe7JAPNEHxhzxDk=
|
||||
github.com/google/osv-scalibr v0.2.1/go.mod h1:gTmbCPgh9ooYnU55N32qPxHgFubkxgiDxsoCAMQc2Nc=
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34=
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -228,6 +236,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk=
|
||||
@@ -321,9 +331,13 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs=
|
||||
github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -653,8 +667,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -663,6 +677,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/extractor"
|
||||
"github.com/safedep/pmg/packagemanager"
|
||||
)
|
||||
|
||||
@@ -73,6 +74,12 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
|
||||
log.Debugf("Running package manager guard with args: %v", args)
|
||||
|
||||
if !parsedCommand.HasInstallTarget() {
|
||||
// Check if this is a manifest-based installation
|
||||
if parsedCommand.ShouldExtractFromManifest() {
|
||||
log.Debugf("Detected manifest-based installation, extracting packages from manifest files")
|
||||
return g.handleManifestInstallation(ctx, parsedCommand)
|
||||
}
|
||||
|
||||
log.Debugf("No install target found, continuing execution")
|
||||
return g.continueExecution(ctx, parsedCommand)
|
||||
}
|
||||
@@ -268,3 +275,59 @@ func (g *packageManagerGuard) clearStatus() {
|
||||
|
||||
g.interaction.ClearStatus()
|
||||
}
|
||||
|
||||
func (g *packageManagerGuard) handleManifestInstallation(ctx context.Context, parsedCommand *packagemanager.ParsedCommand) error {
|
||||
g.setStatus("Extracting packages from manifest files")
|
||||
|
||||
extractorConfig := extractor.NewDefaultExtractorConfig()
|
||||
extractorConfig.ExtractorPackageManager = extractor.PackageManagerName(g.packageManager.Name())
|
||||
|
||||
packageExtractor := extractor.New(*extractorConfig)
|
||||
|
||||
packages, err := packageExtractor.ExtractManifest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract packages from manifest files: %w", err)
|
||||
}
|
||||
|
||||
if len(packages) == 0 {
|
||||
log.Debugf("No packages found in manifest files, continuing execution")
|
||||
return g.continueExecution(ctx, parsedCommand)
|
||||
}
|
||||
|
||||
log.Debugf("Extracted %d packages from manifest files", len(packages))
|
||||
|
||||
// Analyze the extracted packages
|
||||
g.setStatus(fmt.Sprintf("Analyzing %d packages from manifest files", len(packages)))
|
||||
|
||||
analysisResults, err := g.concurrentAnalyzePackages(ctx, packages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to analyze packages: %w", err)
|
||||
}
|
||||
|
||||
confirmableMalwarePackages := []*analyzer.PackageVersionAnalysisResult{}
|
||||
for _, result := range analysisResults {
|
||||
if result.Action == analyzer.ActionBlock {
|
||||
return g.blockInstallation(result)
|
||||
}
|
||||
|
||||
if result.Action == analyzer.ActionConfirm {
|
||||
confirmableMalwarePackages = append(confirmableMalwarePackages, result)
|
||||
}
|
||||
}
|
||||
|
||||
if len(confirmableMalwarePackages) > 0 {
|
||||
confirmed, err := g.getConfirmationOnMalware(ctx, confirmableMalwarePackages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get confirmation on malware: %w", err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return g.blockInstallation(confirmableMalwarePackages...)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("No malicious packages found in manifest files, continuing execution")
|
||||
|
||||
g.clearStatus()
|
||||
return g.continueExecution(ctx, parsedCommand)
|
||||
}
|
||||
|
||||
+29
-7
@@ -40,7 +40,11 @@ func NewNpmPackageManager(config NpmPackageManagerConfig) (*npmPackageManager, e
|
||||
var _ PackageManager = &npmPackageManager{}
|
||||
|
||||
func (npm *npmPackageManager) Name() string {
|
||||
return "npm"
|
||||
return npm.Config.CommandName
|
||||
}
|
||||
|
||||
func (npm *npmPackageManager) Ecosystem() packagev1.Ecosystem {
|
||||
return packagev1.Ecosystem_ECOSYSTEM_NPM
|
||||
}
|
||||
|
||||
func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error) {
|
||||
@@ -50,8 +54,8 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
|
||||
|
||||
command := Command{Exe: npm.Config.CommandName, Args: args}
|
||||
|
||||
// No command specified
|
||||
if len(args) < 2 {
|
||||
// Since manifest-based installs like 'npm i' are now valid commands
|
||||
if len(args) < 1 {
|
||||
return &ParsedCommand{
|
||||
Command: command,
|
||||
}, nil
|
||||
@@ -59,8 +63,12 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
|
||||
|
||||
// Extract packages from args
|
||||
var packages []string
|
||||
var isManifestInstall bool
|
||||
var foundInstallCmd bool
|
||||
|
||||
for idx, arg := range args {
|
||||
if slices.Contains(npm.Config.InstallCommands, arg) {
|
||||
foundInstallCmd = true
|
||||
// All subsequent args are packages except for flags
|
||||
for i := idx + 1; i < len(args); i++ {
|
||||
if strings.HasPrefix(args[i], "-") {
|
||||
@@ -74,8 +82,14 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
|
||||
}
|
||||
}
|
||||
|
||||
// No packages found
|
||||
if len(packages) == 0 {
|
||||
// If install command was found but no explicit packages,
|
||||
// this is a manifest-based installation (install from package.json)
|
||||
if foundInstallCmd && len(packages) == 0 {
|
||||
isManifestInstall = true
|
||||
}
|
||||
|
||||
// No packages found and not a manifest install
|
||||
if len(packages) == 0 && !isManifestInstall {
|
||||
return &ParsedCommand{
|
||||
Command: command,
|
||||
}, nil
|
||||
@@ -105,9 +119,17 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
|
||||
})
|
||||
}
|
||||
|
||||
var manifestFiles []string
|
||||
if isManifestInstall {
|
||||
// npm/pnpm installs from package.json by default
|
||||
manifestFiles = append(manifestFiles, "package.json")
|
||||
}
|
||||
|
||||
return &ParsedCommand{
|
||||
Command: command,
|
||||
InstallTargets: installTargets,
|
||||
Command: command,
|
||||
InstallTargets: installTargets,
|
||||
IsManifestInstall: isManifestInstall,
|
||||
ManifestFiles: manifestFiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -121,3 +121,90 @@ func TestNpmParseCommand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNpmParseCommand_ManifestInstallation(t *testing.T) {
|
||||
pm, err := NewNpmPackageManager(DefaultNpmPackageManagerConfig())
|
||||
assert.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedManifest bool
|
||||
expectedFiles []string
|
||||
expectedTargets int
|
||||
}{
|
||||
{
|
||||
name: "npm install without args (bare install)",
|
||||
args: []string{"install"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"package.json"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "npm i without args (short form)",
|
||||
args: []string{"i"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"package.json"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "npm install with explicit package",
|
||||
args: []string{"install", "react"},
|
||||
expectedManifest: false,
|
||||
expectedFiles: nil,
|
||||
expectedTargets: 1,
|
||||
},
|
||||
{
|
||||
name: "npm install with multiple packages",
|
||||
args: []string{"install", "react", "vue"},
|
||||
expectedManifest: false,
|
||||
expectedFiles: nil,
|
||||
expectedTargets: 2,
|
||||
},
|
||||
{
|
||||
name: "npm install with flags but no packages",
|
||||
args: []string{"install", "--save-dev"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"package.json"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "npm install with mixed args",
|
||||
args: []string{"install", "react", "--save"},
|
||||
expectedManifest: false,
|
||||
expectedFiles: nil,
|
||||
expectedTargets: 1,
|
||||
},
|
||||
{
|
||||
name: "non-install command",
|
||||
args: []string{"run", "build"},
|
||||
expectedManifest: false,
|
||||
expectedFiles: nil,
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "pnpm install without args",
|
||||
args: []string{"install"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"package.json"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
parsed, err := pm.ParseCommand(tc.args)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expectedManifest, parsed.IsManifestInstall, "IsManifestInstall mismatch")
|
||||
assert.Equal(t, tc.expectedFiles, parsed.ManifestFiles, "ManifestFiles mismatch")
|
||||
assert.Equal(t, tc.expectedTargets, len(parsed.InstallTargets), "InstallTargets count mismatch")
|
||||
|
||||
// Test helper methods
|
||||
assert.Equal(t, tc.expectedManifest, parsed.HasManifestInstall(), "HasManifestInstall mismatch")
|
||||
|
||||
expectedShouldExtract := tc.expectedManifest && tc.expectedTargets == 0
|
||||
assert.Equal(t, expectedShouldExtract, parsed.ShouldExtractFromManifest(), "ShouldExtractFromManifest mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,28 @@ type ParsedCommand struct {
|
||||
|
||||
// Parsed install target if this is an install command
|
||||
InstallTargets []*PackageInstallTarget
|
||||
|
||||
// IsManifestInstall indicates if this is a manifest-based installation
|
||||
// (e.g., npm install, pip install -r requirements.txt)
|
||||
IsManifestInstall bool
|
||||
|
||||
// ManifestFiles contains the list of manifest files to install from
|
||||
// (e.g., ["requirements.txt"] for pip install -r requirements.txt)
|
||||
ManifestFiles []string
|
||||
}
|
||||
|
||||
func (pc *ParsedCommand) HasInstallTarget() bool {
|
||||
return len(pc.InstallTargets) > 0
|
||||
}
|
||||
|
||||
func (pc *ParsedCommand) HasManifestInstall() bool {
|
||||
return pc.IsManifestInstall
|
||||
}
|
||||
|
||||
func (pc *ParsedCommand) ShouldExtractFromManifest() bool {
|
||||
return pc.IsManifestInstall && !pc.HasInstallTarget()
|
||||
}
|
||||
|
||||
// PackageManager is the contract for implementing a package manager
|
||||
type PackageManager interface {
|
||||
// Name of the package manager implementation
|
||||
@@ -44,6 +60,9 @@ type PackageManager interface {
|
||||
// ParseCommand parses the command and returns a parsed command
|
||||
// specific to the package manager implementation
|
||||
ParseCommand(args []string) (*ParsedCommand, error)
|
||||
|
||||
// Ecosystem of the package manager
|
||||
Ecosystem() packagev1.Ecosystem
|
||||
}
|
||||
|
||||
// PackageResolver is the contract for resolving package info
|
||||
|
||||
+58
-5
@@ -37,30 +37,81 @@ func (pip *pipPackageManager) Name() string {
|
||||
return "pip"
|
||||
}
|
||||
|
||||
func (pip *pipPackageManager) Ecosystem() packagev1.Ecosystem {
|
||||
return packagev1.Ecosystem_ECOSYSTEM_PYPI
|
||||
}
|
||||
|
||||
func (pip *pipPackageManager) ParseCommand(args []string) (*ParsedCommand, error) {
|
||||
if len(args) > 0 && args[0] == "pip" {
|
||||
args = args[1:]
|
||||
}
|
||||
command := Command{Exe: pip.Config.CommandName, Args: args}
|
||||
|
||||
if len(args) < 2 {
|
||||
// Since manifest-based installs like 'npm i' are now valid commands
|
||||
if len(args) < 1 {
|
||||
return &ParsedCommand{
|
||||
Command: command,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var packages []string
|
||||
var manifestFiles []string
|
||||
var isManifestInstall bool
|
||||
var foundInstallCmd bool
|
||||
|
||||
for idx, arg := range args {
|
||||
if slices.Contains(pip.Config.InstallCommands, arg) {
|
||||
foundInstallCmd = true
|
||||
// Check for manifest-based installation flags
|
||||
for i := idx + 1; i < len(args); i++ {
|
||||
if strings.HasPrefix(args[i], "-") {
|
||||
currentArg := args[i]
|
||||
|
||||
// Handle -r/--requirement flags
|
||||
if currentArg == "-r" || currentArg == "--requirement" {
|
||||
isManifestInstall = true
|
||||
if i+1 < len(args) {
|
||||
manifestFiles = append(manifestFiles, args[i+1])
|
||||
i++ // skip the filename
|
||||
}
|
||||
continue
|
||||
}
|
||||
packages = append(packages, args[i])
|
||||
|
||||
// Handle combined -r flag (e.g., -rrequirements.txt)
|
||||
if strings.HasPrefix(currentArg, "-r") && len(currentArg) > 2 {
|
||||
isManifestInstall = true
|
||||
manifestFiles = append(manifestFiles, currentArg[2:])
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle other flags that indicate manifest installation
|
||||
if currentArg == "-e" || currentArg == "--editable" ||
|
||||
currentArg == "-c" || currentArg == "--constraint" {
|
||||
if i+1 < len(args) {
|
||||
i++ // skip the next argument
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If it's a flag, skip it
|
||||
if strings.HasPrefix(currentArg, "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, it's a package name
|
||||
packages = append(packages, currentArg)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If install command was found but no explicit packages and no manifest flags,
|
||||
// check if it's a bare "pip install" (which should look for default manifest files)
|
||||
if foundInstallCmd && len(packages) == 0 && len(manifestFiles) == 0 {
|
||||
isManifestInstall = true
|
||||
// pip install without args typically looks for requirements.txt
|
||||
manifestFiles = append(manifestFiles, "requirements.txt")
|
||||
}
|
||||
|
||||
var installTargets []*PackageInstallTarget
|
||||
|
||||
for _, pkg := range packages {
|
||||
@@ -96,8 +147,10 @@ func (pip *pipPackageManager) ParseCommand(args []string) (*ParsedCommand, error
|
||||
}
|
||||
|
||||
return &ParsedCommand{
|
||||
Command: command,
|
||||
InstallTargets: installTargets,
|
||||
Command: command,
|
||||
InstallTargets: installTargets,
|
||||
IsManifestInstall: isManifestInstall,
|
||||
ManifestFiles: manifestFiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,93 @@ func TestPipParsePackageInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipParseCommand_ManifestInstallation(t *testing.T) {
|
||||
pm, err := NewPipPackageManager(DefaultPipPackageManagerConfig())
|
||||
assert.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedManifest bool
|
||||
expectedFiles []string
|
||||
expectedTargets int
|
||||
}{
|
||||
{
|
||||
name: "pip install with -r flag",
|
||||
args: []string{"install", "-r", "requirements.txt"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"requirements.txt"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "pip install with --requirement flag",
|
||||
args: []string{"install", "--requirement", "requirements.txt"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"requirements.txt"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "pip install with combined -r flag",
|
||||
args: []string{"install", "-rrequirements.txt"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"requirements.txt"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "pip install without args (bare install)",
|
||||
args: []string{"install"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"requirements.txt"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "pip install with explicit package",
|
||||
args: []string{"install", "django"},
|
||||
expectedManifest: false,
|
||||
expectedFiles: nil,
|
||||
expectedTargets: 1,
|
||||
},
|
||||
{
|
||||
name: "pip install with mixed args",
|
||||
args: []string{"install", "django", "-r", "requirements.txt"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"requirements.txt"},
|
||||
expectedTargets: 1,
|
||||
},
|
||||
{
|
||||
name: "pip install with multiple -r flags",
|
||||
args: []string{"install", "-r", "requirements.txt", "-r", "dev-requirements.txt"},
|
||||
expectedManifest: true,
|
||||
expectedFiles: []string{"requirements.txt", "dev-requirements.txt"},
|
||||
expectedTargets: 0,
|
||||
},
|
||||
{
|
||||
name: "non-install command",
|
||||
args: []string{"list"},
|
||||
expectedManifest: false,
|
||||
expectedFiles: nil,
|
||||
expectedTargets: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
parsed, err := pm.ParseCommand(tc.args)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expectedManifest, parsed.IsManifestInstall, "IsManifestInstall mismatch")
|
||||
assert.Equal(t, tc.expectedFiles, parsed.ManifestFiles, "ManifestFiles mismatch")
|
||||
assert.Equal(t, tc.expectedTargets, len(parsed.InstallTargets), "InstallTargets count mismatch")
|
||||
|
||||
// Test helper methods
|
||||
assert.Equal(t, tc.expectedManifest, parsed.HasManifestInstall(), "HasManifestInstall mismatch")
|
||||
|
||||
expectedShouldExtract := tc.expectedManifest && tc.expectedTargets == 0
|
||||
assert.Equal(t, expectedShouldExtract, parsed.ShouldExtractFromManifest(), "ShouldExtractFromManifest mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipConvertCompatibleRelease(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user