Skip to content
This repository has been archived by the owner on Jun 21, 2022. It is now read-only.

feat(analyzer): support ArchLinux and pacman #280

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions analyzer/all/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
_ "github.com/aquasecurity/fanal/analyzer/language/rust/cargo"
_ "github.com/aquasecurity/fanal/analyzer/os/alpine"
_ "github.com/aquasecurity/fanal/analyzer/os/amazonlinux"
_ "github.com/aquasecurity/fanal/analyzer/os/archlinux"
_ "github.com/aquasecurity/fanal/analyzer/os/debian"
_ "github.com/aquasecurity/fanal/analyzer/os/mariner"
_ "github.com/aquasecurity/fanal/analyzer/os/photon"
Expand All @@ -29,5 +30,6 @@ import (
_ "github.com/aquasecurity/fanal/analyzer/os/ubuntu"
_ "github.com/aquasecurity/fanal/analyzer/pkg/apk"
_ "github.com/aquasecurity/fanal/analyzer/pkg/dpkg"
_ "github.com/aquasecurity/fanal/analyzer/pkg/pacman"
_ "github.com/aquasecurity/fanal/analyzer/pkg/rpm"
)
8 changes: 5 additions & 3 deletions analyzer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const (
// ======
TypeAlpine Type = "alpine"
TypeAmazon Type = "amazon"
TypeArch Type = "arch"
TypeCBLMariner Type = "cbl-mariner"
TypeDebian Type = "debian"
TypePhoton Type = "photon"
Expand All @@ -21,9 +22,10 @@ const (
TypeUbuntu Type = "ubuntu"

// OS Package
TypeApk Type = "apk"
TypeDpkg Type = "dpkg"
TypeRpm Type = "rpm"
TypeApk Type = "apk"
TypeDpkg Type = "dpkg"
TypePacman Type = "pacman"
TypeRpm Type = "rpm"

// ============================
// Programming Language Package
Expand Down
55 changes: 55 additions & 0 deletions analyzer/os/archlinux/archlinux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package archlinux

import (
"bufio"
"context"
"os"
"strings"

"github.com/aquasecurity/fanal/analyzer"
aos "github.com/aquasecurity/fanal/analyzer/os"
"github.com/aquasecurity/fanal/types"
"github.com/aquasecurity/fanal/utils"
"golang.org/x/xerrors"
)

func init() {
analyzer.RegisterAnalyzer(&archlinuxOSAnalyzer{})
}

const version = 1

var requiredFiles = []string{
"usr/lib/os-release",
"etc/os-release",
}

type archlinuxOSAnalyzer struct{}

func (a archlinuxOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
scanner := bufio.NewScanner(input.Content)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "NAME=\"Arch Linux") {
return &analyzer.AnalysisResult{
OS: &types.OS{
Family: aos.Arch,
Name: "Arch Linux",
},
}, nil
}
}
return nil, xerrors.Errorf("arch: %w", aos.AnalyzeOSError)
}

func (a archlinuxOSAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return utils.StringInSlice(filePath, requiredFiles)
}

func (a archlinuxOSAnalyzer) Type() analyzer.Type {
return analyzer.TypeArch
}

func (a archlinuxOSAnalyzer) Version() int {
return version
}
57 changes: 57 additions & 0 deletions analyzer/os/archlinux/archlinux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package archlinux

import (
"context"
"os"
"testing"

"github.com/aquasecurity/fanal/analyzer"
aos "github.com/aquasecurity/fanal/analyzer/os"
"github.com/aquasecurity/fanal/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_archlinuxOSAnalyzer_Analyze(t *testing.T) {
tests := []struct {
name string
inputFile string
want *analyzer.AnalysisResult
wantErr string
}{
{
name: "happy path with ArchLinux",
inputFile: "testdata/archlinux/os-release",
want: &analyzer.AnalysisResult{
OS: &types.OS{Family: aos.Arch, Name: "Arch Linux"},
},
},
{
name: "sad path",
inputFile: "testdata/not_archlinux/os-release",
wantErr: "arch: unable to analyze OS information",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := archlinuxOSAnalyzer{}
f, err := os.Open(tt.inputFile)
require.NoError(t, err)
defer f.Close()

ctx := context.Background()

got, err := a.Analyze(ctx, analyzer.AnalysisInput{
FilePath: "etc/os-release",
Content: f,
})
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
10 changes: 10 additions & 0 deletions analyzer/os/archlinux/testdata/archlinux/os-release
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://bugs.archlinux.org/"
LOGO=archlinux
1 change: 1 addition & 0 deletions analyzer/os/archlinux/testdata/not_archlinux/os-release
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Red Hat Linux release 6.2 (Zoot)
3 changes: 3 additions & 0 deletions analyzer/os/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ const (

// Alpine is done
Alpine = "alpine"

// Arch is done
Arch = "arch"
)

var AnalyzeOSError = xerrors.New("unable to analyze OS information")
163 changes: 163 additions & 0 deletions analyzer/pkg/pacman/pacman.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package pacman

import (
"bufio"
"context"
"log"
"os"
"path/filepath"
"strconv"
"strings"

pacmanVersion "github.com/MaineK00n/go-pacman-version"

"github.com/aquasecurity/fanal/analyzer"
"github.com/aquasecurity/fanal/types"
"golang.org/x/xerrors"
)

func init() {
analyzer.RegisterAnalyzer(&pacmanAnalyzer{})
}

const version = 1

const installDir = "var/lib/pacman/local/"

type pacmanAnalyzer struct{}

func (a pacmanAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
scanner := bufio.NewScanner(input.Content)
dir, fileName := filepath.Split(input.FilePath)
if !strings.HasPrefix(dir, installDir) {
return nil, nil
}
if fileName == "desc" {
pkg, err := a.parsePacmanPkgDesc(scanner)
if err != nil {
return nil, xerrors.Errorf("failed to parse desc: %w", err)
}
return &analyzer.AnalysisResult{
PackageInfos: []types.PackageInfo{
{FilePath: input.FilePath, Packages: []types.Package{pkg}},
},
}, nil
}
if fileName == "files" {
result, err := a.parsePacmanPkgFiles(scanner)
if err != nil {
return nil, xerrors.Errorf("failed to parse files: %w", err)
}
return result, nil
}
return nil, nil
}

func (a pacmanAnalyzer) parsePacmanPkgDesc(scanner *bufio.Scanner) (types.Package, error) {
var pkg types.Package
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "%NAME%") {
if scanner.Scan() {
pkg.Name = scanner.Text()
}
} else if strings.HasPrefix(line, "%VERSION%") {
if scanner.Scan() {
version := scanner.Text()
if !pacmanVersion.Valid(version) {
log.Printf("Invalid Version Found : OS %s, Package %s, Version %s", "arch", pkg.Name, version)
continue
}
splitted := strings.SplitN(version, ":", 2)
if len(splitted) == 1 {
pkg.Epoch = 0
version = splitted[0]
} else {
var err error
pkg.Epoch, err = strconv.Atoi(splitted[0])
if err != nil {
return types.Package{}, xerrors.Errorf("failed to convert epoch: %w", err)
}

if pkg.Epoch < 0 {
return types.Package{}, xerrors.Errorf("epoch is negative")
}
version = splitted[1]
}

index := strings.Index(version, "-")
if index >= 0 {
ver := version[:index]
rel := version[index+1:]
pkg.Version = ver
pkg.Release = rel
pkg.SrcVersion = ver
pkg.SrcRelease = rel
} else {
pkg.Version = version
pkg.SrcVersion = version
}
}
} else if strings.HasPrefix(line, "%BASE%") {
if scanner.Scan() {
pkg.SrcName = scanner.Text()
}
} else if strings.HasPrefix(line, "%ARCH%") {
if scanner.Scan() {
pkg.Arch = scanner.Text()
}
} else if strings.HasPrefix(line, "%LICENSE%") {
if scanner.Scan() {
pkg.License = scanner.Text()
}
}
}

if err := scanner.Err(); err != nil {
return types.Package{}, xerrors.Errorf("scan error: %w", err)
}

return pkg, nil
}

// parsePacmanPkgFiles parses /var/lib/pacman/local/*/files
func (a pacmanAnalyzer) parsePacmanPkgFiles(scanner *bufio.Scanner) (*analyzer.AnalysisResult, error) {
var installedFiles []string
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "%FILES%") {
continue
}
if strings.HasPrefix(line, "%BACKUP%") {
break
}

if _, fileName := filepath.Split(line); fileName != "" {
installedFiles = append(installedFiles, line)
}
}

if err := scanner.Err(); err != nil {
return nil, xerrors.Errorf("scan error: %w", err)
}

return &analyzer.AnalysisResult{
SystemInstalledFiles: installedFiles,
}, nil
}

func (a pacmanAnalyzer) Required(filePath string, _ os.FileInfo) bool {
dir, fileName := filepath.Split(filePath)
if !strings.HasPrefix(dir, installDir) {
return false
}
return fileName == "desc" || fileName == "files"
}

func (a pacmanAnalyzer) Type() analyzer.Type {
return analyzer.TypePacman
}

func (a pacmanAnalyzer) Version() int {
return version
}
Loading