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

/*
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")
}