Drew Short
3 years ago
7 changed files with 349 additions and 14 deletions
-
12cmd/check.go
-
3go.mod
-
18go.sum
-
201internal/parser/debian.go
-
52internal/parser/dockerfile/dockerfile.go
-
70internal/parser/parser.go
-
5test/data/old_pinned_dependencies_ubuntu.Dockerfile
@ -0,0 +1,201 @@ |
|||||
|
/* |
||||
|
Copyright © 2021 Drew Short <warrick@sothr.com> |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
package parser |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"errors" |
||||
|
"strings" |
||||
|
|
||||
|
log "github.com/sirupsen/logrus" |
||||
|
|
||||
|
"mvdan.cc/sh/v3/syntax" |
||||
|
) |
||||
|
|
||||
|
// DebianCommandParser the parser responsible for handling Debian based images
|
||||
|
type DebianCommandParser struct { |
||||
|
parser *syntax.Parser |
||||
|
printer *syntax.Printer |
||||
|
} |
||||
|
|
||||
|
// newDebianCommandParser Provision a new instance of DebianCommandParser with required components
|
||||
|
func newDebianCommandParser() DebianCommandParser { |
||||
|
return DebianCommandParser{ |
||||
|
parser: syntax.NewParser(), |
||||
|
printer: syntax.NewPrinter(), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// shellNodeToString Convert a node to the string representation
|
||||
|
func (c DebianCommandParser) shellNodeToString(node syntax.Node) string { |
||||
|
buffer := bytes.NewBufferString("") |
||||
|
err := c.printer.Print(buffer, node) |
||||
|
if err != nil { |
||||
|
log.Panic("Failed to convert node to string") |
||||
|
} |
||||
|
return buffer.String() |
||||
|
} |
||||
|
|
||||
|
// isRedirectedToAptSourcesList Checks if the list of redirects contains the apt sources list directory
|
||||
|
func isRedirectedToAptSourcesList(redirects []*syntax.Redirect) bool { |
||||
|
if redirects != nil && len(redirects) > 0 { |
||||
|
for _, redirect := range redirects { |
||||
|
if strings.Contains(redirect.Word.Lit(), "/etc/apt/sources.list") { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// isAddRepositoryExpression Checks for expressions that add to the repository
|
||||
|
func isAddRepositoryExpression(command syntax.CallExpr, redirects []*syntax.Redirect) bool { |
||||
|
// Adding via debian tooling
|
||||
|
if command.Args[0].Lit() == "add-apt-repository" { |
||||
|
return true |
||||
|
// Echoing into a sources file directly
|
||||
|
} else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// trimQuotes return a string without surrounding quotes
|
||||
|
func trimQuotes(value string) string { |
||||
|
if value[0] == '"' && value[len(value)-1] == '"' { |
||||
|
return value[1 : len(value)-1] |
||||
|
} else if value[0] == '\'' && value[len(value)-1] == '\'' { |
||||
|
return value[1 : len(value)-1] |
||||
|
} |
||||
|
return value |
||||
|
} |
||||
|
|
||||
|
// getAddedRepository parse the repository information from the shell commands
|
||||
|
func (c DebianCommandParser) getAddedRepository(command syntax.CallExpr, redirects []*syntax.Redirect) (string, error) { |
||||
|
// Adding via debian tooling
|
||||
|
if command.Args[0].Lit() == "add-apt-repository" { |
||||
|
return command.Args[len(command.Args)-1].Lit(), nil |
||||
|
// Echoing into a sources file directly
|
||||
|
} else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) { |
||||
|
for _, argument := range command.Args { |
||||
|
var argumentNode syntax.Node = argument |
||||
|
argumentValue := trimQuotes(c.shellNodeToString(argumentNode)) |
||||
|
if strings.Contains(argumentValue, "deb") || strings.Contains(argumentValue, "http://") { |
||||
|
return argumentValue, nil |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return "", errors.New("failed to parse repository") |
||||
|
} |
||||
|
|
||||
|
// GetRepositories Returns a list of Repository entries that were identified
|
||||
|
func (c DebianCommandParser) GetRepositories(command string) ([]*Repository, error) { |
||||
|
var repositories []*Repository |
||||
|
|
||||
|
reader := strings.NewReader(command) |
||||
|
parsedScript, err := c.parser.Parse(reader, "") |
||||
|
if err != nil { |
||||
|
log.Fatalf("Failed to parse script \"%s\"", command) |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
syntax.Walk(parsedScript, func(node syntax.Node) bool { |
||||
|
switch parsedStatement := node.(type) { |
||||
|
case *syntax.Stmt: |
||||
|
switch parsedCommand := parsedStatement.Cmd.(type) { |
||||
|
case *syntax.CallExpr: |
||||
|
if isAddRepositoryExpression(*parsedCommand, parsedStatement.Redirs) { |
||||
|
repositoryInformation, err := c.getAddedRepository(*parsedCommand, parsedStatement.Redirs) |
||||
|
if err != nil { |
||||
|
log.Fatalf("Unexpected error while parsing repository from \"%s\": %s", c.shellNodeToString(node), err) |
||||
|
} |
||||
|
repositories = append(repositories, &Repository{Information: repositoryInformation}) |
||||
|
log.Debugf("Found repository \"%s\"", repositoryInformation) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
}) |
||||
|
|
||||
|
return repositories, nil |
||||
|
} |
||||
|
|
||||
|
// isAddRepositoryExpression Checks for expressions that add to the repository
|
||||
|
func isInstallerExpression(command syntax.CallExpr) bool { |
||||
|
// Adding via debian tooling
|
||||
|
commandProgram := command.Args[0].Lit() |
||||
|
if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (c DebianCommandParser) getInstalledPinnedPackages(command syntax.CallExpr) ([]*Package, error) { |
||||
|
var packages []*Package |
||||
|
|
||||
|
// Adding via debian tooling
|
||||
|
commandProgram := command.Args[0].Lit() |
||||
|
if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" { |
||||
|
for _, argument := range command.Args { |
||||
|
var argumentNode syntax.Node = argument |
||||
|
argumentValue := trimQuotes(c.shellNodeToString(argumentNode)) |
||||
|
if strings.Contains(argumentValue, "=") { |
||||
|
pinnedPackageParts := strings.SplitN(argumentValue, "=", 2) |
||||
|
packages = append(packages, &Package{ |
||||
|
Name: pinnedPackageParts[0], |
||||
|
Version: pinnedPackageParts[1], |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
return packages, nil |
||||
|
} |
||||
|
return packages, errors.New("failed to parse packages") |
||||
|
} |
||||
|
|
||||
|
// GetPinnedPackages get a list of all pinned packages
|
||||
|
func (c DebianCommandParser) GetPinnedPackages(command string) ([]*Package, error) { |
||||
|
var packages []*Package |
||||
|
|
||||
|
reader := strings.NewReader(command) |
||||
|
parsedScript, err := c.parser.Parse(reader, "") |
||||
|
if err != nil { |
||||
|
log.Fatalf("Failed to parse script \"%s\"", command) |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
syntax.Walk(parsedScript, func(node syntax.Node) bool { |
||||
|
switch parsedStatement := node.(type) { |
||||
|
case *syntax.Stmt: |
||||
|
switch parsedCommand := parsedStatement.Cmd.(type) { |
||||
|
case *syntax.CallExpr: |
||||
|
if isInstallerExpression(*parsedCommand) { |
||||
|
pinnedPackages, err := c.getInstalledPinnedPackages(*parsedCommand) |
||||
|
if err != nil { |
||||
|
log.Fatalf("Unexpected error while parsing pinned packages from \"%s\": %s", c.shellNodeToString(node), err) |
||||
|
} |
||||
|
for _, pinnedPackage := range pinnedPackages { |
||||
|
packages = append(packages, pinnedPackage) |
||||
|
log.Debugf("Found pinned package \"%s=%s\"", pinnedPackage.Name, pinnedPackage.Version) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
}) |
||||
|
|
||||
|
return packages, nil |
||||
|
} |
@ -1,7 +1,8 @@ |
|||||
FROM ubuntu:18.04 |
FROM ubuntu:18.04 |
||||
|
|
||||
RUN apt-get update && apt-get install \ |
|
||||
|
RUN echo "deb http://archive.canonical.com/ubuntu bionic partner" > /etc/apt/sources.list.d/partner.list \ |
||||
|
&& apt-get update && apt-get install \ |
||||
ca-certificates=20210119~18.04.1 \ |
ca-certificates=20210119~18.04.1 \ |
||||
curl=7.58.0-2ubuntu3.15 \ |
curl=7.58.0-2ubuntu3.15 \ |
||||
&& apt-get clean \ |
&& apt-get clean \ |
||||
&& rm -rf /var/lib/apt/lists/* |
|
||||
|
&& rm -rf /var/lib/apt/lists/* \ |
Write
Preview
Loading…
Cancel
Save
Reference in new issue