Skip to content

Commit ffb5c85

Browse files
authored
feat(analyzer): support Red Hat build info (fanal#151)
1 parent 533498f commit ffb5c85

22 files changed

+810
-161
lines changed

.github/workflows/bench.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ jobs:
1010
uses: actions/setup-go@v2
1111
with:
1212
go-version: 1.17
13-
id: go
1413

1514
- name: Check out code into the Go module directory
1615
uses: actions/checkout@v2

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ jobs:
3131
uses: actions/setup-go@v2
3232
with:
3333
go-version: ${{ env.GO_VERSION }}
34-
id: go
3534

3635
- name: Check out code into the Go module directory
3736
uses: actions/checkout@v2

analyzer/all/import.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package all
22

33
import (
4+
_ "github.com/aquasecurity/fanal/analyzer/buildinfo"
45
_ "github.com/aquasecurity/fanal/analyzer/command/apk"
56
_ "github.com/aquasecurity/fanal/analyzer/language/dotnet/nuget"
67
_ "github.com/aquasecurity/fanal/analyzer/language/golang/binary"

analyzer/analyzer.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,17 @@ type AnalysisResult struct {
8383
Configs []types.Config
8484
SystemInstalledFiles []string // A list of files installed by OS package manager
8585

86+
// For Red Hat
87+
BuildInfo *types.BuildInfo
88+
8689
// CustomResources hold analysis results from custom analyzers.
8790
// It is for extensibility and not used in OSS.
8891
CustomResources []types.CustomResource
8992
}
9093

9194
func (r *AnalysisResult) isEmpty() bool {
9295
return r.OS == nil && len(r.PackageInfos) == 0 && len(r.Applications) == 0 &&
93-
len(r.Configs) == 0 && len(r.SystemInstalledFiles) == 0 && len(r.CustomResources) == 0
96+
len(r.Configs) == 0 && len(r.SystemInstalledFiles) == 0 && r.BuildInfo == nil && len(r.CustomResources) == 0
9497
}
9598

9699
func (r *AnalysisResult) Sort() {
@@ -148,6 +151,22 @@ func (r *AnalysisResult) Merge(new *AnalysisResult) {
148151

149152
r.SystemInstalledFiles = append(r.SystemInstalledFiles, new.SystemInstalledFiles...)
150153

154+
if new.BuildInfo != nil {
155+
if r.BuildInfo == nil {
156+
r.BuildInfo = new.BuildInfo
157+
} else {
158+
// We don't need to merge build info here
159+
// because there is theoretically only one file about build info in each layer.
160+
if new.BuildInfo.Nvr != "" || new.BuildInfo.Arch != "" {
161+
r.BuildInfo.Nvr = new.BuildInfo.Nvr
162+
r.BuildInfo.Arch = new.BuildInfo.Arch
163+
}
164+
if len(new.BuildInfo.ContentSets) > 0 {
165+
r.BuildInfo.ContentSets = new.BuildInfo.ContentSets
166+
}
167+
}
168+
}
169+
151170
r.CustomResources = append(r.CustomResources, new.CustomResources...)
152171
}
153172

analyzer/analyzer_test.go

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ import (
1414
"golang.org/x/xerrors"
1515

1616
"github.com/aquasecurity/fanal/analyzer"
17-
_ "github.com/aquasecurity/fanal/analyzer/all"
1817
aos "github.com/aquasecurity/fanal/analyzer/os"
19-
_ "github.com/aquasecurity/fanal/hook/all"
2018
"github.com/aquasecurity/fanal/types"
2119
dio "github.com/aquasecurity/go-dep-parser/pkg/io"
20+
21+
_ "github.com/aquasecurity/fanal/analyzer/command/apk"
22+
_ "github.com/aquasecurity/fanal/analyzer/language/ruby/bundler"
23+
_ "github.com/aquasecurity/fanal/analyzer/os/alpine"
24+
_ "github.com/aquasecurity/fanal/analyzer/os/ubuntu"
25+
_ "github.com/aquasecurity/fanal/analyzer/pkg/apk"
26+
_ "github.com/aquasecurity/fanal/hook/all"
2227
)
2328

2429
type mockConfigAnalyzer struct{}
@@ -453,72 +458,18 @@ func TestAnalyzer_AnalyzerVersions(t *testing.T) {
453458
name: "happy path",
454459
disabled: []analyzer.Type{},
455460
want: map[string]int{
456-
"alpine": 1,
457-
"amazon": 1,
458-
"apk": 1,
459-
"bundler": 1,
460-
"cargo": 1,
461-
"centos": 1,
462-
"rocky": 1,
463-
"alma": 1,
464-
"composer": 1,
465-
"debian": 1,
466-
"dpkg": 2,
467-
"fedora": 1,
468-
"gobinary": 1,
469-
"gomod": 1,
470-
"jar": 1,
471-
"node-pkg": 1,
472-
"npm": 1,
473-
"nuget": 2,
474-
"oracle": 1,
475-
"photon": 1,
476-
"pip": 1,
477-
"pipenv": 1,
478-
"poetry": 1,
479-
"pom": 1,
480-
"redhat": 1,
481-
"rpm": 1,
482-
"suse": 1,
483-
"ubuntu": 1,
484-
"yarn": 1,
485-
"python-pkg": 1,
486-
"gemspec": 1,
461+
"alpine": 1,
462+
"apk": 1,
463+
"bundler": 1,
464+
"ubuntu": 1,
487465
},
488466
},
489467
{
490468
name: "disable analyzers",
491469
disabled: []analyzer.Type{analyzer.TypeAlpine, analyzer.TypeUbuntu},
492470
want: map[string]int{
493-
"amazon": 1,
494-
"apk": 1,
495-
"bundler": 1,
496-
"cargo": 1,
497-
"centos": 1,
498-
"rocky": 1,
499-
"alma": 1,
500-
"composer": 1,
501-
"debian": 1,
502-
"dpkg": 2,
503-
"fedora": 1,
504-
"gobinary": 1,
505-
"gomod": 1,
506-
"jar": 1,
507-
"node-pkg": 1,
508-
"npm": 1,
509-
"nuget": 2,
510-
"oracle": 1,
511-
"photon": 1,
512-
"pip": 1,
513-
"pipenv": 1,
514-
"poetry": 1,
515-
"pom": 1,
516-
"redhat": 1,
517-
"rpm": 1,
518-
"suse": 1,
519-
"yarn": 1,
520-
"python-pkg": 1,
521-
"gemspec": 1,
471+
"apk": 1,
472+
"bundler": 1,
522473
},
523474
},
524475
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package buildinfo
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
"path/filepath"
8+
9+
"golang.org/x/xerrors"
10+
11+
"github.com/aquasecurity/fanal/analyzer"
12+
"github.com/aquasecurity/fanal/types"
13+
)
14+
15+
func init() {
16+
analyzer.RegisterAnalyzer(&contentManifestAnalyzer{})
17+
}
18+
19+
const contentManifestAnalyzerVersion = 1
20+
21+
type contentManifest struct {
22+
ContentSets []string `json:"content_sets"`
23+
}
24+
25+
// For Red Hat products
26+
type contentManifestAnalyzer struct{}
27+
28+
func (a contentManifestAnalyzer) Analyze(_ context.Context, target analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
29+
var manifest contentManifest
30+
if err := json.NewDecoder(target.Content).Decode(&manifest); err != nil {
31+
return nil, xerrors.Errorf("invalid content manifest: %w", err)
32+
}
33+
34+
return &analyzer.AnalysisResult{
35+
BuildInfo: &types.BuildInfo{
36+
ContentSets: manifest.ContentSets,
37+
},
38+
}, nil
39+
}
40+
41+
func (a contentManifestAnalyzer) Required(filePath string, _ os.FileInfo) bool {
42+
dir, file := filepath.Split(filepath.ToSlash(filePath))
43+
if dir != "root/buildinfo/content_manifests/" {
44+
return false
45+
}
46+
return filepath.Ext(file) == ".json"
47+
}
48+
49+
func (a contentManifestAnalyzer) Type() analyzer.Type {
50+
return analyzer.TypeRedHatContentManifestType
51+
}
52+
53+
func (a contentManifestAnalyzer) Version() int {
54+
return contentManifestAnalyzerVersion
55+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package buildinfo
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/aquasecurity/fanal/analyzer"
12+
"github.com/aquasecurity/fanal/types"
13+
)
14+
15+
func Test_contentManifestAnalyzer_Analyze(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
input string
19+
want *analyzer.AnalysisResult
20+
wantErr string
21+
}{
22+
{
23+
name: "happy path",
24+
input: "testdata/content_manifests/ubi8-minimal-container-8.5-218.json",
25+
want: &analyzer.AnalysisResult{
26+
BuildInfo: &types.BuildInfo{
27+
ContentSets: []string{
28+
"rhel-8-for-x86_64-baseos-rpms",
29+
"rhel-8-for-x86_64-appstream-rpms",
30+
},
31+
},
32+
},
33+
},
34+
{
35+
name: "broken json",
36+
input: "testdata/content_manifests/broken.json",
37+
wantErr: "invalid content manifest",
38+
},
39+
}
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
f, err := os.Open(tt.input)
43+
require.NoError(t, err)
44+
defer f.Close()
45+
46+
a := contentManifestAnalyzer{}
47+
got, err := a.Analyze(context.Background(), analyzer.AnalysisInput{
48+
FilePath: tt.input,
49+
Content: f,
50+
})
51+
52+
if tt.wantErr != "" {
53+
require.Error(t, err)
54+
assert.Contains(t, err.Error(), tt.wantErr)
55+
return
56+
}
57+
58+
require.NoError(t, err)
59+
assert.Equal(t, tt.want, got)
60+
})
61+
}
62+
}
63+
64+
func Test_contentManifestAnalyzer_Required(t *testing.T) {
65+
tests := []struct {
66+
name string
67+
filePath string
68+
want bool
69+
}{
70+
{
71+
name: "happy path",
72+
filePath: "root/buildinfo/content_manifests/nodejs-12-container-1-66.json",
73+
want: true,
74+
},
75+
{
76+
name: "sad path",
77+
filePath: "root/buildinfo/content_manifests/nodejs-12-container-1-66.xml",
78+
want: false,
79+
},
80+
}
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
a := contentManifestAnalyzer{}
84+
got := a.Required(tt.filePath, nil)
85+
assert.Equal(t, tt.want, got)
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)