Skip to content

Commit 9052808

Browse files
committed
feat: [CN-296] Support Container Base Image Recommendations
Adding a new Container LS to support the upgrading the container base images in a docker file.
1 parent 39bced9 commit 9052808

File tree

18 files changed

+1424
-7
lines changed

18 files changed

+1424
-7
lines changed

application/config/client_settings.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import (
2222
)
2323

2424
const (
25-
ActivateSnykOssKey = "ACTIVATE_SNYK_OPEN_SOURCE"
26-
ActivateSnykCodeKey = "ACTIVATE_SNYK_CODE"
27-
ActivateSnykIacKey = "ACTIVATE_SNYK_IAC"
28-
ActivateSnykAdvisorKey = "ACTIVATE_SNYK_ADVISOR"
29-
SendErrorReportsKey = "SEND_ERROR_REPORTS"
30-
Organization = "SNYK_CFG_ORG"
25+
ActivateSnykOssKey = "ACTIVATE_SNYK_OPEN_SOURCE"
26+
ActivateSnykCodeKey = "ACTIVATE_SNYK_CODE"
27+
ActivateSnykIacKey = "ACTIVATE_SNYK_IAC"
28+
ActivateSnykContainerKey = "ACTIVATE_SNYK_CONTAINER"
29+
ActivateSnykAdvisorKey = "ACTIVATE_SNYK_ADVISOR"
30+
SendErrorReportsKey = "SEND_ERROR_REPORTS"
31+
Organization = "SNYK_CFG_ORG"
3132
)
3233

3334
func (c *Config) clientSettingsFromEnv() {
@@ -56,6 +57,7 @@ func (c *Config) productEnablementFromEnv() {
5657
oss := os.Getenv(ActivateSnykOssKey)
5758
code := os.Getenv(ActivateSnykCodeKey)
5859
iac := os.Getenv(ActivateSnykIacKey)
60+
container := os.Getenv(ActivateSnykContainerKey)
5961
advisor := os.Getenv(ActivateSnykAdvisorKey)
6062

6163
if oss != "" {
@@ -82,6 +84,14 @@ func (c *Config) productEnablementFromEnv() {
8284
c.SetSnykIacEnabled(parseBool)
8385
}
8486

87+
if container != "" {
88+
parseBool, err := strconv.ParseBool(container)
89+
if err != nil {
90+
c.Logger().Debug().Err(err).Str("method", "clientSettingsFromEnv").Msgf("couldn't parse container config %s", container)
91+
}
92+
c.SetSnykContainerEnabled(parseBool)
93+
}
94+
8595
if advisor != "" {
8696
parseBool, err := strconv.ParseBool(advisor)
8797
if err != nil {

application/config/config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ type Config struct {
163163
isSnykCodeEnabled bool
164164
isSnykOssEnabled bool
165165
isSnykIacEnabled bool
166+
isSnykContainerEnabled bool
166167
isSnykAdvisorEnabled bool
167168
manageBinariesAutomatically bool
168169
logPath string
@@ -262,6 +263,7 @@ func newConfig(engine workflow.Engine, opts ...ConfigOption) *Config {
262263
c.isErrorReportingEnabled = true
263264
c.isSnykOssEnabled = true
264265
c.isSnykIacEnabled = true
266+
c.isSnykContainerEnabled = true
265267
c.manageBinariesAutomatically = true
266268
c.logPath = ""
267269
c.snykCodeAnalysisTimeout = c.snykCodeAnalysisTimeoutFromEnv()
@@ -432,6 +434,13 @@ func (c *Config) IsSnykIacEnabled() bool {
432434
return c.isSnykIacEnabled
433435
}
434436

437+
func (c *Config) IsSnykContainerEnabled() bool {
438+
c.m.RLock()
439+
defer c.m.RUnlock()
440+
441+
return c.isSnykContainerEnabled
442+
}
443+
435444
func (c *Config) IsSnykAdvisorEnabled() bool {
436445
c.m.RLock()
437446
defer c.m.RUnlock()
@@ -604,6 +613,13 @@ func (c *Config) SetSnykIacEnabled(enabled bool) {
604613
c.isSnykIacEnabled = enabled
605614
}
606615

616+
func (c *Config) SetSnykContainerEnabled(enabled bool) {
617+
c.m.Lock()
618+
defer c.m.Unlock()
619+
620+
c.isSnykContainerEnabled = enabled
621+
}
622+
607623
func (c *Config) SetSnykAdvisorEnabled(enabled bool) {
608624
c.m.Lock()
609625
defer c.m.Unlock()

application/config/product.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ func (c *Config) IsProductEnabled(p product.Product) bool {
2626
return c.IsSnykOssEnabled()
2727
case product.ProductInfrastructureAsCode:
2828
return c.IsSnykIacEnabled()
29+
case product.ProductContainer:
30+
return c.IsSnykContainerEnabled()
2931
default:
3032
return false
3133
}
@@ -38,6 +40,7 @@ func (c *Config) DisplayableIssueTypes() map[product.FilterableIssueType]bool {
3840
// Handle backwards compatibility.
3941
enabled[product.FilterableIssueTypeCodeSecurity] = c.IsSnykCodeEnabled() || c.IsSnykCodeSecurityEnabled()
4042
enabled[product.FilterableIssueTypeInfrastructureAsCode] = c.IsSnykIacEnabled()
43+
enabled[product.FilterableIssueTypeContainer] = c.IsSnykContainerEnabled()
4144

4245
return enabled
4346
}

application/di/init.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/snyk/snyk-ls/infrastructure/cli/cli_constants"
4242
"github.com/snyk/snyk-ls/infrastructure/cli/install"
4343
"github.com/snyk/snyk-ls/infrastructure/code"
44+
"github.com/snyk/snyk-ls/infrastructure/container"
4445
"github.com/snyk/snyk-ls/infrastructure/iac"
4546
"github.com/snyk/snyk-ls/infrastructure/learn"
4647
"github.com/snyk/snyk-ls/infrastructure/oss"
@@ -55,6 +56,7 @@ import (
5556
var snykApiClient snyk_api.SnykApiClient
5657
var snykCodeScanner *code.Scanner
5758
var infrastructureAsCodeScanner *iac.Scanner
59+
var containerScanner types.ProductScanner
5860
var openSourceScanner types.ProductScanner
5961
var scanInitializer initialize.Initializer
6062
var authenticationService authentication.AuthenticationService
@@ -88,7 +90,7 @@ func Init() {
8890

8991
func initDomain(c *config.Config) {
9092
hoverService = hover.NewDefaultService(c)
91-
scanner = scanner2.NewDelegatingScanner(c, scanInitializer, instrumentor, scanNotifier, snykApiClient, authenticationService, notifier, scanPersister, scanStateAggregator, snykCodeScanner, infrastructureAsCodeScanner, openSourceScanner)
93+
scanner = scanner2.NewDelegatingScanner(c, scanInitializer, instrumentor, scanNotifier, snykApiClient, authenticationService, notifier, scanPersister, scanStateAggregator, snykCodeScanner, infrastructureAsCodeScanner, containerScanner, openSourceScanner)
9294
}
9395

9496
func initInfrastructure(c *config.Config) {
@@ -136,6 +138,7 @@ func initInfrastructure(c *config.Config) {
136138
)
137139

138140
infrastructureAsCodeScanner = iac.New(c, instrumentor, errorReporter, snykCli)
141+
containerScanner = container.New(c, instrumentor, errorReporter, networkAccess)
139142
openSourceScanner = oss.NewCLIScanner(c, instrumentor, errorReporter, snykCli, learnService, notifier)
140143
scanNotifier, _ = appNotification.NewScanNotifier(c, notifier)
141144
snykCodeScanner = code.New(c, instrumentor, snykApiClient, codeErrorReporter, learnService, notifier, codeClientScanner)

application/server/configuration.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ func updateProductEnablement(c *config.Config, settings types.Settings) {
402402
} else {
403403
c.SetSnykIacEnabled(parseBool)
404404
}
405+
parseBool, err = strconv.ParseBool(settings.ActivateSnykContainer)
406+
if err != nil {
407+
c.Logger().Debug().Msg("couldn't parse container setting")
408+
} else {
409+
c.SetSnykContainerEnabled(parseBool)
410+
}
405411
}
406412

407413
func updateIssueViewOptions(c *config.Config, s *types.IssueViewOptions) {

application/server/configuration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ func Test_UpdateSettings(t *testing.T) {
177177
ActivateSnykOpenSource: "false",
178178
ActivateSnykCode: "false",
179179
ActivateSnykIac: "false",
180+
ActivateSnykContainer: "false",
180181
Insecure: "true",
181182
Endpoint: "https://api.snyk.io",
182183
AdditionalParams: "--all-projects -d",
@@ -224,6 +225,7 @@ func Test_UpdateSettings(t *testing.T) {
224225
assert.Equal(t, false, c.IsSnykCodeEnabled())
225226
assert.Equal(t, false, c.IsSnykOssEnabled())
226227
assert.Equal(t, false, c.IsSnykIacEnabled())
228+
assert.Equal(t, false, c.IsSnykContainerEnabled())
227229
assert.Equal(t, true, c.CliSettings().Insecure)
228230
assert.Equal(t, []string{"--all-projects", "-d"}, c.CliSettings().AdditionalOssParameters)
229231
assert.Equal(t, "https://api.snyk.io", c.SnykApi())

ast/dockerfile/parser.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* © 2025 Snyk Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dockerfile
18+
19+
import (
20+
"regexp"
21+
"strings"
22+
23+
"github.com/rs/zerolog"
24+
25+
"github.com/snyk/snyk-ls/ast"
26+
"github.com/snyk/snyk-ls/internal/types"
27+
)
28+
29+
type Parser struct {
30+
logger zerolog.Logger
31+
}
32+
33+
var (
34+
// fromRegex matches FROM instructions in Dockerfile
35+
fromRegex = regexp.MustCompile(`(?i)^\s*FROM\s+([^\s]+)`)
36+
)
37+
38+
func New(logger *zerolog.Logger) Parser {
39+
return Parser{
40+
logger: *logger,
41+
}
42+
}
43+
44+
func (p *Parser) Parse(content []byte, uri string) *ast.Tree {
45+
tree := p.initTree(types.FilePath(uri), string(content))
46+
47+
lines := strings.Split(strings.ReplaceAll(string(content), "\r", ""), "\n")
48+
for lineNum, line := range lines {
49+
matches := fromRegex.FindStringSubmatch(line)
50+
if matches == nil {
51+
continue
52+
}
53+
54+
baseImage := matches[1]
55+
// Skip scratch base image
56+
if baseImage == "scratch" {
57+
continue
58+
}
59+
60+
node := p.addFromNode(tree.Root, lineNum, line, baseImage)
61+
p.logger.Debug().Interface("nodeName", node.Name).Str("path", tree.Document).Msg("Added FROM node")
62+
}
63+
64+
return tree
65+
}
66+
67+
func (p *Parser) initTree(path types.FilePath, content string) *ast.Tree {
68+
root := ast.Node{
69+
Line: 0,
70+
StartChar: 0,
71+
EndChar: -1,
72+
DocOffset: 0,
73+
Parent: nil,
74+
Children: nil,
75+
Name: string(path),
76+
Value: content,
77+
}
78+
79+
root.Tree = &ast.Tree{
80+
Root: &root,
81+
Document: string(path),
82+
}
83+
return root.Tree
84+
}
85+
86+
func (p *Parser) addFromNode(parent *ast.Node, lineNum int, line string, baseImage string) *ast.Node {
87+
// Find the position of the base image in the line
88+
fromIndex := strings.Index(strings.ToLower(line), "from")
89+
imageStart := fromIndex + 4 // "FROM" length
90+
for imageStart < len(line) && line[imageStart] == ' ' {
91+
imageStart++ // Skip whitespace
92+
}
93+
endChar := imageStart + len(baseImage)
94+
95+
node := ast.Node{
96+
Line: lineNum,
97+
StartChar: imageStart,
98+
EndChar: endChar,
99+
DocOffset: int64(fromIndex),
100+
Parent: parent,
101+
Children: nil,
102+
Name: "FROM",
103+
Value: baseImage,
104+
Attributes: make(map[string]string),
105+
Tree: parent.Tree,
106+
}
107+
108+
parent.Add(&node)
109+
return &node
110+
}

0 commit comments

Comments
 (0)