Skip to content

Conversation

@dan-arpino
Copy link
Contributor

@dan-arpino dan-arpino commented Sep 8, 2025

Adding a new Container LS to support the upgrading the container base images in a docker file.

Description

Adding base image recommendations to the IDE. This will scan docker files for base images and check if we have any base image upgrade recommendations CN-296

Checklist

  • Tests added and all succeed
  • Regenerated mocks, etc. (make generate)
  • Linted (make lint-fix)
  • README.md updated, if user-facing
  • License file updated, if new 3rd-party dependency is introduced

@CLAassistant
Copy link

CLAassistant commented Sep 8, 2025

CLA assistant check
All committers have signed the CLA.

@snyk-io
Copy link

snyk-io bot commented Sep 8, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

license/snyk check is complete. No issues have been found. (View Details)

code/snyk check is complete. No issues have been found. (View Details)

agg.referenceScanStates[folderProductKey{Product: product.ProductOpenSource, FolderPath: folderPath}] = &scanState{Status: NotStarted}
agg.referenceScanStates[folderProductKey{Product: product.ProductCode, FolderPath: folderPath}] = &scanState{Status: NotStarted}
agg.referenceScanStates[folderProductKey{Product: product.ProductInfrastructureAsCode, FolderPath: folderPath}] = &scanState{Status: NotStarted}
agg.referenceScanStates[folderProductKey{Product: product.ProductContainer, FolderPath: folderPath}] = &scanState{Status: NotStarted}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does your implementation support delta scanning? If not, this probably needs a revision.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The whole API client functionality should be a workflow in https://github.com/snyk/go-application-framework and used / imported here. That would enable re-use in the CLI and migration away from the legacy CLI functionality through using a go-workflow.


// Add headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-snyk-ide", "snyk-ls-"+config.Version)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This may already be done automatically during LS initialization, I assume this does not need to be done manually here.

apiURL := fmt.Sprintf("%s/docker-deps/recommended-base-image", d.config.SnykApi())

// Create the request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
Copy link
Collaborator

Choose a reason for hiding this comment

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

As container scanning is a long running process (I think), is there a way to report progress e.g. polling progress from the API?

Comment on lines 102 to 112
// Add authentication header
if d.config.NonEmptyToken() {
if d.config.AuthenticationMethod() == types.OAuthAuthentication {
oAuthToken, tokenErr := d.config.TokenAsOAuthToken()
if tokenErr == nil {
req.Header.Set("Authorization", "Bearer "+oAuthToken.AccessToken)
}
} else {
req.Header.Set("Authorization", "token "+d.config.Token())
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is done automatically, if you use an authenticated client. No need to add the header here.

func (sc *Scanner) Scan(ctx context.Context, path types.FilePath, folderPath types.FilePath, folderConfig *types.FolderConfig) (issues []types.Issue, err error) {
fileInfo, err := os.Stat(string(path))
if err != nil {
sc.errorReporter.CaptureError(err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Better use CaptureErrorAsIssue

issues := make([]types.Issue, 0)

// Regex to match FROM instructions in Dockerfile
fromRegex := regexp.MustCompile(`(?i)^\s*FROM\s+([^\s]+)`)
Copy link
Collaborator

Choose a reason for hiding this comment

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

premature optimization from my side: this should be a global variable/constant

lowerFileName == "docker-compose.yaml"
}

func (sc *Scanner) scanDockerFile(ctx context.Context, filePath string, content string) ([]types.Issue, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is missing context, requestID & progress handling. Have a look at the other scanners for that :)

Comment on lines 223 to 288
for lineNum, line := range lines {
matches := fromRegex.FindStringSubmatch(line)
if matches == nil {
continue
}

baseImage := matches[1]
// Skip scratch and multi-stage build references
if baseImage == "scratch" || strings.Contains(baseImage, " as ") {
continue
}

// Get recommendation from API
recommendation, err := sc.apiClient.GetBaseImageRecommendation(ctx, baseImage)
if err != nil {
sc.errorReporter.CaptureError(err)
log.Err(err).Str("method", "container.scanDockerFile").Msg("Error getting base image recommendation")
continue
}

// Create issue with code action to replace base image
issue := sc.createBaseImageIssue(filePath, lineNum, line, baseImage, recommendation)
issues = append(issues, issue)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd restructure the logic into: input (=image extraction), processing (api call), output (conversion to issues)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, I'd parallelize the API calls with goroutines.


func (sc *Scanner) createBaseImageIssue(filePath string, lineNum int, line string, baseImage string, recommendation *BaseImageRecommendation) types.Issue {
// Find the position of the base image in the line
fromIndex := strings.Index(strings.ToLower(line), "from")
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's an AST package, you could use to create an AST out of the dockerfile and use that afterwards. We already do that for maven. You would add a dockerfile parser and a Parser interface that is implemented by both Maven parser and Dockerfile parser with Parse(content string, path types.FilePath) *ast.Tree as function. WDYT?

}

codeAction, err := snyk.NewCodeAction(
fmt.Sprintf("Upgrade to %s", recommendation.RecommendedImage),
Copy link
Collaborator

Choose a reason for hiding this comment

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

We may want to add (Snyk) at the end of the action title.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, in Open Source, we add the number of issues that would be fixed, and the number of issues that would remain in the text. Having that for container, would be great!

// Create issue
issue := &snyk.Issue{
ID: fmt.Sprintf("SNYK-CONTAINER-BASE-IMAGE-%s", baseImage),
Severity: types.Medium,
Copy link
Collaborator

Choose a reason for hiding this comment

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

why always medium?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd suggest the highest severity of an issue found within the baseImage


// Create issue
issue := &snyk.Issue{
ID: fmt.Sprintf("SNYK-CONTAINER-BASE-IMAGE-%s", baseImage),
Copy link
Collaborator

Choose a reason for hiding this comment

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

the ID should be unique - why only one issue per baseImage?

Message: fmt.Sprintf("Base image '%s' may have security vulnerabilities", baseImage),
FormattedMessage: sc.getFormattedMessage(baseImage, recommendation),
AffectedFilePath: types.FilePath(filePath),
ContentRoot: types.FilePath(filePath),
Copy link
Collaborator

Choose a reason for hiding this comment

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

ContentRoot must be the folder

}

func TestScanner_Product(t *testing.T) {
c := config.New()
Copy link
Collaborator

Choose a reason for hiding this comment

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

we have test helpers that help with setting up a new config, e.g. c := testutil.UnitTest(t) for a unit test or c:= testutil.SmokeTest(t) for a smoke test that tests E2E.

Adding a new Container LS to support the upgrading the container base images in a docker file.
@dan-arpino dan-arpino force-pushed the feat/CN-296-container-base-image-rec branch from 38fe982 to 9052808 Compare October 1, 2025 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants