Skip to content

Commit 0f8ac99

Browse files
feat(python): add support for requirements.txt (fanal#219)
1 parent 380c05b commit 0f8ac99

File tree

12 files changed

+197
-53
lines changed

12 files changed

+197
-53
lines changed

analyzer/all/import.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
_ "github.com/aquasecurity/fanal/analyzer/library/jar"
1111
_ "github.com/aquasecurity/fanal/analyzer/library/npm"
1212
_ "github.com/aquasecurity/fanal/analyzer/library/nuget"
13+
_ "github.com/aquasecurity/fanal/analyzer/library/pip"
1314
_ "github.com/aquasecurity/fanal/analyzer/library/pipenv"
1415
_ "github.com/aquasecurity/fanal/analyzer/library/poetry"
1516
_ "github.com/aquasecurity/fanal/analyzer/library/yarn"

analyzer/analyzer_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ func TestAnalyzer_AnalyzerVersions(t *testing.T) {
507507
"nuget": 1,
508508
"oracle": 1,
509509
"photon": 1,
510+
"pip": 1,
510511
"pipenv": 1,
511512
"poetry": 1,
512513
"redhat": 1,
@@ -537,6 +538,7 @@ func TestAnalyzer_AnalyzerVersions(t *testing.T) {
537538
"nuget": 1,
538539
"oracle": 1,
539540
"photon": 1,
541+
"pip": 1,
540542
"pipenv": 1,
541543
"poetry": 1,
542544
"redhat": 1,

analyzer/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
TypeNpm Type = "npm"
2929
TypeNuget Type = "nuget"
3030
TypePipenv Type = "pipenv"
31+
TypePip Type = "pip"
3132
TypePoetry Type = "poetry"
3233
TypeYarn Type = "yarn"
3334
TypeGoBinary Type = "gobinary"

analyzer/library/pip/pip.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package pip
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/aquasecurity/fanal/analyzer"
8+
"github.com/aquasecurity/fanal/analyzer/library"
9+
"github.com/aquasecurity/fanal/types"
10+
"github.com/aquasecurity/go-dep-parser/pkg/pip"
11+
"golang.org/x/xerrors"
12+
)
13+
14+
func init() {
15+
analyzer.RegisterAnalyzer(&pipLibraryAnalyzer{})
16+
}
17+
18+
const version = 1
19+
20+
var requiredFile = "requirements.txt"
21+
22+
type pipLibraryAnalyzer struct{}
23+
24+
func (a pipLibraryAnalyzer) Analyze(target analyzer.AnalysisTarget) (*analyzer.AnalysisResult, error) {
25+
res, err := library.Analyze(types.Pip, target.FilePath, target.Content, pip.Parse)
26+
if err != nil {
27+
return nil, xerrors.Errorf("unable to parse requirements.txt: %w", err)
28+
}
29+
return res, nil
30+
}
31+
32+
func (a pipLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool {
33+
fileName := filepath.Base(filePath)
34+
return fileName == requiredFile
35+
}
36+
37+
func (a pipLibraryAnalyzer) Type() analyzer.Type {
38+
return analyzer.TypePip
39+
}
40+
41+
func (a pipLibraryAnalyzer) Version() int {
42+
return version
43+
}

analyzer/library/pip/pip_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package pip
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/aquasecurity/fanal/analyzer"
11+
"github.com/aquasecurity/fanal/types"
12+
godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
13+
)
14+
15+
func Test_pipAnalyzer_Analyze(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
inputFile string
19+
want *analyzer.AnalysisResult
20+
wantErr string
21+
}{
22+
{
23+
name: "happy path",
24+
inputFile: "testdata/requirements.txt",
25+
want: &analyzer.AnalysisResult{
26+
Applications: []types.Application{
27+
{
28+
Type: types.Pip,
29+
FilePath: "testdata/requirements.txt",
30+
Libraries: []types.LibraryInfo{
31+
{Library: godeptypes.Library{Name: "click", Version: "8.0.0"}},
32+
{Library: godeptypes.Library{Name: "Flask", Version: "2.0.0"}},
33+
{Library: godeptypes.Library{Name: "itsdangerous", Version: "2.0.0"}},
34+
},
35+
},
36+
},
37+
},
38+
}, {
39+
name: "happy path with not related filename",
40+
inputFile: "testdata/not-related.txt",
41+
want: nil,
42+
},
43+
}
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
b, err := os.ReadFile(tt.inputFile)
47+
require.NoError(t, err)
48+
49+
a := pipLibraryAnalyzer{}
50+
got, err := a.Analyze(analyzer.AnalysisTarget{
51+
FilePath: tt.inputFile,
52+
Content: b,
53+
})
54+
55+
if tt.wantErr != "" {
56+
require.NotNil(t, err)
57+
assert.Contains(t, err.Error(), tt.wantErr)
58+
return
59+
}
60+
assert.NoError(t, err)
61+
assert.Equal(t, tt.want, got)
62+
})
63+
}
64+
}
65+
66+
func Test_pipAnalyzer_Required(t *testing.T) {
67+
tests := []struct {
68+
name string
69+
filePath string
70+
want bool
71+
}{
72+
{
73+
name: "happy",
74+
filePath: "test/requirements.txt",
75+
want: true,
76+
},
77+
{
78+
name: "sad",
79+
filePath: "a/b/c/d/test.sum",
80+
want: false,
81+
},
82+
}
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
a := pipLibraryAnalyzer{}
86+
got := a.Required(tt.filePath, nil)
87+
assert.Equal(t, tt.want, got)
88+
})
89+
}
90+
}

analyzer/library/pip/testdata/not-related.txt

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
click==8.0.0
2+
Flask==2.0.0
3+
itsdangerous==2.0.0
4+
Jinja2<3.0.0
5+
MarkupSafe>2.0.0
6+
Werkzeug

0 commit comments

Comments
 (0)