Skip to content
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
39 changes: 36 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,46 @@
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg/mod \
go run ./cmd/docgen

FROM busybox
# Download xeol EOL DB at build time (offline at runtime). Listing serves .tar.xz URLs.
RUN apk add --no-cache curl jq xz && \

Check warning on line 28 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L28

Only the exit code from the final command in this RUN instruction will be evaluated unless 'pipefail' is set.
XEOL_DB_URL=$(curl -sSfL https://data.xeol.io/xeol/databases/listing.json | jq -r '.available["1"] | .[-1] | .url') && \
curl -sSfL "$XEOL_DB_URL" -o /tmp/xeol-db.tar.xz && \
mkdir -p /src/xeol-db/1 && tar -xJf /tmp/xeol-db.tar.xz -C /src/xeol-db/1 && \
rm /tmp/xeol-db.tar.xz

RUN adduser -u 2004 -D docker
# Download Trivy vuln DB at build time so slim image can run EOL scan (runner still needs DB to init).
WORKDIR /src/trivy-cache/db
RUN ORAS_VER=1.1.0 && \

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Codacy found a medium BestPractice issue: Use WORKDIR to switch to a directory

The issue identified by the Hadolint linter is related to the use of the RUN command without setting a working directory using WORKDIR. When you use RUN to execute commands, it's generally a good practice to set a specific working directory with WORKDIR to ensure that the commands are executed in the intended context. This enhances readability and maintainability of the Dockerfile.

To fix the issue, you can add a WORKDIR instruction before the RUN command that sets the ORAS_VER variable. Here's the code suggestion:

Suggested change
RUN ORAS_VER=1.1.0 && \
WORKDIR /src/trivy-cache

This comment was generated by an experimental AI tool.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 Codacy found a high ErrorProne issue: Note that A && B || C is not if-then-else. C may run when A is true.

The issue identified by Hadolint is related to the use of the && and || operators in the command. The warning indicates that the way the commands are structured could lead to unintended execution of the command after the || operator. Specifically, if the command before || (which is meant to be a fallback or alternative) runs successfully, the command after || might still execute, which is not the intended behavior.

To fix this issue, we can use a more explicit structure that clearly defines the intended logic. In this case, we can ensure that the commands are grouped correctly to avoid unexpected executions.

Here’s the suggested code change:

RUN ORAS_VER=1.1.0 && \
    curl -sSfL "https://github.com/oras-project/oras/releases/download/v${ORAS_VER}/oras_${ORAS_VER}_linux_amd64.tar.gz" -o /tmp/oras.tar.gz && \
    tar -xzf /tmp/oras.tar.gz -C /usr/local/bin oras && rm /tmp/oras.tar.gz && \
    mkdir -p /src/trivy-cache/db && cd /src/trivy-cache/db && \
    oras pull ghcr.io/aquasecurity/trivy-db:2 && \
    (test -f db.tar.gz && tar -xzf db.tar.gz && rm -f db.tar.gz && mv 2/* . && rmdir 2) || true

In this change, I've combined the mv and rmdir commands into the same command group to ensure they only execute if the test command is successful, maintaining the intended control flow.


This comment was generated by an experimental AI tool.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codacy found an issue: Use WORKDIR to switch to a directory

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curl -sSfL "https://github.com/oras-project/oras/releases/download/v${ORAS_VER}/oras_${ORAS_VER}_linux_amd64.tar.gz" -o /tmp/oras.tar.gz && \
tar -xzf /tmp/oras.tar.gz -C /usr/local/bin oras && rm /tmp/oras.tar.gz && \
mkdir -p /src/trivy-cache/db && \
oras pull ghcr.io/aquasecurity/trivy-db:2 && \
( (test -f db.tar.gz && tar -xzf db.tar.gz && rm -f db.tar.gz) || true ) && \
( (test -d 2 && mv 2/* . 2>/dev/null && rmdir 2 2>/dev/null) || true )

# Build eoltest for container verification (optional).
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg/mod \
go build -o bin/eoltest ./cmd/eoltest

FROM busybox:1.36 AS full
RUN adduser -u 2004 -D docker
COPY --from=builder --chown=docker:docker /src/bin /dist/bin
COPY --from=builder --chown=docker:docker /src/docs /docs
COPY --from=builder --chown=docker:docker /src/docs /docs
COPY --chown=docker:docker cache/ /dist/cache/codacy-trivy
COPY --chown=docker:docker openssf-malicious-packages/openssf-malicious-packages-index.json.gz /dist/cache/codacy-trivy/openssf-malicious-packages-index.json.gz
COPY --from=builder --chown=docker:docker /src/xeol-db /dist/cache/xeol/db
ENV XEOL_DB_CACHE_DIR=/dist/cache/xeol/db
CMD [ "/dist/bin/codacy-trivy" ]

# Slim: no host cache/openssf; includes Trivy DB + xeol DB for EOL scan. Use: docker build --target slim -t codacy-trivy:eol .
FROM busybox:1.36 AS slim
RUN adduser -u 2004 -D docker
COPY --from=builder --chown=docker:docker /src/bin /dist/bin
COPY --from=builder --chown=docker:docker /src/docs /docs
RUN mkdir -p /dist/cache/codacy-trivy
COPY --from=builder --chown=docker:docker /src/trivy-cache/db /dist/cache/codacy-trivy/db
COPY --from=builder --chown=docker:docker /src/xeol-db /dist/cache/xeol/db
ENV XEOL_DB_CACHE_DIR=/dist/cache/xeol/db
CMD [ "/dist/bin/codacy-trivy" ]

FROM full

Check warning on line 69 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L69

Detected docker image with no explicit version attached.
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Build and test require Go 1.25+ and GOEXPERIMENT=jsonv2 (for Trivy).
# CGO_ENABLED=0 avoids linking system libs (e.g. faiss on macOS).
export GOEXPERIMENT := jsonv2
export GOTOOLCHAIN := auto
export CGO_ENABLED := 0

.PHONY: build test
build:
go build -o bin/codacy-trivy -ldflags="-s -w" ./cmd/tool

test:
go test ./...
142 changes: 142 additions & 0 deletions cmd/eoltest/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Program eoltest runs the codacy-trivy tool against a directory with EOL patterns
// for local testing. EOL scan uses the xeol Go library (no xeol binary needed).
// Requires (for full scan) a valid malicious-packages index; uses an empty index if the default path is missing.
//
// Usage: go run ./cmd/eoltest -dir ./test-eol-project
package main

import (
"compress/gzip"
"context"
"flag"
"fmt"
"io"
"os"
"path/filepath"

codacy "github.com/codacy/codacy-engine-golang-seed/v8"
"github.com/codacy/codacy-trivy/internal/tool"
)

func main() {
dir := flag.String("dir", "", "Source directory to scan (e.g. test-eol-project)")
flag.Parse()
if *dir == "" {
fmt.Fprintln(os.Stderr, "usage: eoltest -dir <path>")
os.Exit(1)
}
absDir, err := filepath.Abs(*dir)
if err != nil {
fmt.Fprintf(os.Stderr, "dir: %v\n", err)
os.Exit(1)
}
if _, err := os.Stat(absDir); err != nil {
fmt.Fprintf(os.Stderr, "dir %s: %v\n", absDir, err)
os.Exit(1)
}
indexPath, cleanup, err := resolveIndexPath()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
defer cleanup()
results, err := runEOLScan(context.Background(), absDir, indexPath)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
printEOLResults(results)
}

// resolveIndexPath returns the malicious-packages index path and a cleanup function.
// If the default path exists it is returned with a no-op cleanup; otherwise a temp empty index is created.
func resolveIndexPath() (path string, cleanup func(), err error) {
return resolveIndexPathWithDefault(tool.MaliciousPackagesIndexPath)
}

// resolveIndexPathWithDefault is the testable core; defaultPath is the path to check first (e.g. tool.MaliciousPackagesIndexPath).
func resolveIndexPathWithDefault(defaultPath string) (path string, cleanup func(), err error) {
if _, statErr := os.Stat(defaultPath); statErr == nil {
return defaultPath, func() {}, nil
}
f, err := os.CreateTemp("", "codacy-trivy-malicious-*.json.gz")
if err != nil {
return "", nil, fmt.Errorf("temp index: %w", err)
}
gw := gzip.NewWriter(f)
_, _ = gw.Write([]byte("{}"))
_ = gw.Close()
_ = f.Close()
return f.Name(), func() { os.Remove(f.Name()) }, nil
}

// runEOLScan runs the tool with EOL patterns only and returns all results.
func runEOLScan(ctx context.Context, dir, indexPath string) ([]codacy.Result, error) {
trivy, err := tool.New(indexPath)
if err != nil {
return nil, fmt.Errorf("New: %w", err)
}
files := listFiles(dir)
patterns := []codacy.Pattern{
{ID: "eol_critical"},
{ID: "eol_high"},
{ID: "eol_medium"},
{ID: "eol_minor"},
}
te := codacy.ToolExecution{SourceDir: dir, Patterns: &patterns, Files: &files}
return trivy.Run(ctx, te)
}

// printEOLResults prints EOL issues and file errors from results, then a summary line.
func printEOLResults(results []codacy.Result) {
printEOLResultsTo(os.Stdout, os.Stderr, results)
}

func defaultWriter(w, d io.Writer) io.Writer {
if w == nil {
return d
}
return w
}

// printEOLResultsTo is the testable core; if stdout or stderr is nil, os.Stdout or os.Stderr is used.
func printEOLResultsTo(stdout, stderr io.Writer, results []codacy.Result) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Codacy found a medium Complexity issue: Method printEOLResultsTo has a cyclomatic complexity of 8 (limit is 7)

The issue identified by the Lizard linter pertains to the cyclomatic complexity of the printEOLResultsTo function. Cyclomatic complexity is a measure of the number of linearly independent paths through a program's source code. A high complexity indicates that the function may be doing too much, making it harder to understand, maintain, and test.

In this case, the complexity of 8 exceeds the recommended limit of 7, primarily due to the multiple conditional checks and the loop that processes the results.

To reduce the complexity, one approach is to simplify the handling of the stdout and stderr parameters. Instead of checking if they are nil and then assigning them to os.Stdout or os.Stderr, we can use a helper function to encapsulate that logic. This can be done in a single line change.

Here’s the code suggestion to address the complexity issue:

Suggested change
func printEOLResultsTo(stdout, stderr io.Writer, results []codacy.Result) {
stdout, stderr = io.Writer(os.Stdout), io.Writer(os.Stderr) // Assign default if nil

This change simplifies the assignment logic into one line, effectively reducing the cyclomatic complexity of the function.


This comment was generated by an experimental AI tool.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdout = defaultWriter(stdout, os.Stdout)
stderr = defaultWriter(stderr, os.Stderr)
var count int
for _, r := range results {
switch v := r.(type) {
case codacy.Issue:
if isEOL(v.PatternID) {
count++
fmt.Fprintf(stdout, "%s:%d [%s] %s\n", v.File, v.Line, v.PatternID, v.Message)
}
case codacy.FileError:
if v.File != "" {
fmt.Fprintf(stderr, "file error %s: %s\n", v.File, v.Message)
}
}
}
if count == 0 {
fmt.Fprintln(stdout, "No EOL issues found. Ensure the project has EOL deps (e.g. npm install in test-eol-project) and XEOL_DB_CACHE_DIR is set or DB is in default cache.")
} else {
fmt.Fprintf(stdout, "\nTotal EOL issues: %d\n", count)
}
}

func listFiles(dir string) []string {
var out []string
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
rel, _ := filepath.Rel(dir, path)
out = append(out, rel)
return nil
})
return out
}

func isEOL(id string) bool {
return id == "eol_critical" || id == "eol_high" || id == "eol_medium" || id == "eol_minor"
}
69 changes: 69 additions & 0 deletions cmd/eoltest/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"bytes"
"os"
"path/filepath"
"testing"

codacy "github.com/codacy/codacy-engine-golang-seed/v8"
"github.com/stretchr/testify/assert"
)

func TestResolveIndexPathWithDefault_WhenPathExists(t *testing.T) {
dir := t.TempDir()
existing := filepath.Join(dir, "index.json.gz")
f, err := os.Create(existing)
assert.NoError(t, err)
assert.NoError(t, f.Close())

path, cleanup, err := resolveIndexPathWithDefault(existing)
assert.NoError(t, err)
defer cleanup()
assert.Equal(t, existing, path)
}

func TestResolveIndexPathWithDefault_WhenPathNotExists(t *testing.T) {
path, cleanup, err := resolveIndexPathWithDefault(filepath.Join(t.TempDir(), "nonexistent.json.gz"))
assert.NoError(t, err)
defer cleanup()
assert.NotEmpty(t, path)
_, err = os.Stat(path)
assert.NoError(t, err)
cleanup()
_, err = os.Stat(path)
assert.True(t, os.IsNotExist(err))
}

func TestPrintEOLResultsTo_Empty(t *testing.T) {
var buf bytes.Buffer
printEOLResultsTo(&buf, &buf, nil)
assert.Contains(t, buf.String(), "No EOL issues found")
}

func TestPrintEOLResultsTo_OneIssue(t *testing.T) {
var buf bytes.Buffer
printEOLResultsTo(&buf, &buf, []codacy.Result{
codacy.Issue{File: "go.mod", Line: 5, PatternID: "eol_critical", Message: "EOL pkg"},
})
out := buf.String()
assert.Contains(t, out, "go.mod:5 [eol_critical] EOL pkg")
assert.Contains(t, out, "Total EOL issues: 1")
}

func TestPrintEOLResultsTo_FileError(t *testing.T) {
var stdout, stderr bytes.Buffer
printEOLResultsTo(&stdout, &stderr, []codacy.Result{
codacy.FileError{File: "bad.txt", Message: "read failed"},
})
assert.Contains(t, stderr.String(), "file error bad.txt: read failed")
}

func TestIsEOL(t *testing.T) {
assert.True(t, isEOL("eol_critical"))
assert.True(t, isEOL("eol_high"))
assert.True(t, isEOL("eol_medium"))
assert.True(t, isEOL("eol_minor"))
assert.False(t, isEOL("vulnerability_high"))
assert.False(t, isEOL(""))
}
101 changes: 101 additions & 0 deletions cmd/openssfbuild/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"

"github.com/codacy/codacy-trivy/internal/openssfdb"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)

const (
defaultRepoURL = "https://github.com/ossf/malicious-packages.git"
defaultOutput = "/dist/cache/openssf-malicious-packages.json.gz"
defaultRef = ""
defaultSourceID = "OpenSSF Malicious Packages DB"
)

func main() {
repoURL := flag.String("repo", defaultRepoURL, "OpenSSF malicious packages repository URL")
repoDir := flag.String("repo-dir", "", "Pre-cloned OpenSSF repository to reuse instead of cloning")
ref := flag.String("ref", defaultRef, "Specific git reference to checkout when cloning the repository")
output := flag.String("out", defaultOutput, "Destination path for the gzipped JSON payload")

flag.Parse()

ctx := context.Background()

var (
sourcePath string
sourceMeta string
cleanup func()
err error
)

switch {
case *repoDir != "":
sourcePath, err = filepath.Abs(*repoDir)
if err != nil {
log.Fatalf("resolve repo dir: %v", err)
}
sourceMeta = fmt.Sprintf("%s (local)", defaultSourceID)
default:
sourcePath, cleanup, sourceMeta, err = cloneRepository(ctx, *repoURL, *ref)
if err != nil {
log.Fatalf("clone repository: %v", err)
}
defer cleanup()
}

builder := openssfdb.NewBuilder()

outputPayload, err := builder.Build(ctx, sourcePath, sourceMeta)
if err != nil {
log.Fatalf("build payload: %v", err)
}

if err := openssfdb.WriteGzippedJSON(*output, outputPayload); err != nil {
log.Fatalf("write payload: %v", err)
}
}

func cloneRepository(ctx context.Context, repoURL, ref string) (string, func(), string, error) {
tmpDir, err := os.MkdirTemp("", "openssf-malicious-*")
if err != nil {
return "", nil, "", err
}
cleanup := func() {
_ = os.RemoveAll(tmpDir)
}

options := &git.CloneOptions{
URL: repoURL,
Depth: 1,
}
if ref != "" {
options.ReferenceName = plumbing.ReferenceName(ref)
}

repo, err := git.PlainCloneContext(ctx, tmpDir, false, options)
if err != nil {
cleanup()
return "", nil, "", err
}

sourceMeta := fmt.Sprintf("%s@%s", repoURL, resolveHeadHash(repo))

return tmpDir, cleanup, sourceMeta, nil
}

func resolveHeadHash(repo *git.Repository) string {
headRef, err := repo.Head()
if err != nil {
return "unknown"
}
return headRef.Hash().String()
}
Loading