You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
7.4 KiB
231 lines
7.4 KiB
/*
|
|
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
|
|
}
|
|
|
|
// 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, scriptParseErr := c.parser.Parse(reader, "")
|
|
if scriptParseErr != nil {
|
|
log.Fatalf("Failed to parse script \"%s\": %s", command, scriptParseErr)
|
|
return nil, scriptParseErr
|
|
}
|
|
|
|
var hasErrorWhileWalkingScript bool = false
|
|
var errorWhileWalkingScript error
|
|
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, repositoryParseErr := c.getAddedRepository(*parsedCommand, parsedStatement.Redirs)
|
|
if repositoryParseErr != nil {
|
|
log.Fatalf("Unexpected error while parsing repository from \"%s\": %s", c.shellNodeToString(node), repositoryParseErr)
|
|
hasErrorWhileWalkingScript = true
|
|
errorWhileWalkingScript = repositoryParseErr
|
|
// Terminate walking early so we can raise an error
|
|
return false
|
|
}
|
|
repositories = append(repositories, &Repository{Information: repositoryInformation})
|
|
log.Debugf("Found repository \"%s\"", repositoryInformation)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
if hasErrorWhileWalkingScript {
|
|
return repositories, errorWhileWalkingScript
|
|
}
|
|
|
|
return repositories, nil
|
|
}
|
|
|
|
// GetPinnedPackages get a list of all pinned packages
|
|
func (c DebianCommandParser) GetPinnedPackages(command string) ([]*Package, error) {
|
|
var packages []*Package
|
|
|
|
reader := strings.NewReader(command)
|
|
parsedScript, scriptParseErr := c.parser.Parse(reader, "")
|
|
if scriptParseErr != nil {
|
|
log.Fatalf("Failed to parse script \"%s\"", command)
|
|
return nil, scriptParseErr
|
|
}
|
|
|
|
var hasErrorWhileWalkingScript bool = false
|
|
var errorWhileWalkingScript error
|
|
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, pinnedPackageParseErr := c.getInstalledPinnedPackages(*parsedCommand)
|
|
if pinnedPackageParseErr != nil {
|
|
log.Fatalf("Unexpected error while parsing pinned packages from \"%s\": %s", c.shellNodeToString(node), pinnedPackageParseErr)
|
|
hasErrorWhileWalkingScript = true
|
|
errorWhileWalkingScript = pinnedPackageParseErr
|
|
// Terminate walking early so we can raise an error
|
|
return false
|
|
}
|
|
for _, pinnedPackage := range pinnedPackages {
|
|
packages = append(packages, pinnedPackage)
|
|
log.Debugf("Found pinned package \"%s=%s\"", pinnedPackage.Name, pinnedPackage.Version)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
if hasErrorWhileWalkingScript {
|
|
return packages, errorWhileWalkingScript
|
|
}
|
|
|
|
return packages, nil
|
|
}
|
|
|
|
/*
|
|
General Helpers
|
|
*/
|
|
|
|
// 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()
|
|
}
|
|
|
|
// trimQuotes return a string without surrounding matching 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
|
|
}
|
|
|
|
/*
|
|
Repository Helpers
|
|
*/
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
/*
|
|
Pinned Package Helpers
|
|
*/
|
|
|
|
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")
|
|
}
|