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.

201 lines
6.5 KiB

  1. /*
  2. Copyright © 2021 Drew Short <warrick@sothr.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package parser
  14. import (
  15. "bytes"
  16. "errors"
  17. "strings"
  18. log "github.com/sirupsen/logrus"
  19. "mvdan.cc/sh/v3/syntax"
  20. )
  21. // DebianCommandParser the parser responsible for handling Debian based images
  22. type DebianCommandParser struct {
  23. parser *syntax.Parser
  24. printer *syntax.Printer
  25. }
  26. // newDebianCommandParser Provision a new instance of DebianCommandParser with required components
  27. func newDebianCommandParser() DebianCommandParser {
  28. return DebianCommandParser{
  29. parser: syntax.NewParser(),
  30. printer: syntax.NewPrinter(),
  31. }
  32. }
  33. // shellNodeToString Convert a node to the string representation
  34. func (c DebianCommandParser) shellNodeToString(node syntax.Node) string {
  35. buffer := bytes.NewBufferString("")
  36. err := c.printer.Print(buffer, node)
  37. if err != nil {
  38. log.Panic("Failed to convert node to string")
  39. }
  40. return buffer.String()
  41. }
  42. // isRedirectedToAptSourcesList Checks if the list of redirects contains the apt sources list directory
  43. func isRedirectedToAptSourcesList(redirects []*syntax.Redirect) bool {
  44. if redirects != nil && len(redirects) > 0 {
  45. for _, redirect := range redirects {
  46. if strings.Contains(redirect.Word.Lit(), "/etc/apt/sources.list") {
  47. return true
  48. }
  49. }
  50. }
  51. return false
  52. }
  53. // isAddRepositoryExpression Checks for expressions that add to the repository
  54. func isAddRepositoryExpression(command syntax.CallExpr, redirects []*syntax.Redirect) bool {
  55. // Adding via debian tooling
  56. if command.Args[0].Lit() == "add-apt-repository" {
  57. return true
  58. // Echoing into a sources file directly
  59. } else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) {
  60. return true
  61. }
  62. return false
  63. }
  64. // trimQuotes return a string without surrounding quotes
  65. func trimQuotes(value string) string {
  66. if value[0] == '"' && value[len(value)-1] == '"' {
  67. return value[1 : len(value)-1]
  68. } else if value[0] == '\'' && value[len(value)-1] == '\'' {
  69. return value[1 : len(value)-1]
  70. }
  71. return value
  72. }
  73. // getAddedRepository parse the repository information from the shell commands
  74. func (c DebianCommandParser) getAddedRepository(command syntax.CallExpr, redirects []*syntax.Redirect) (string, error) {
  75. // Adding via debian tooling
  76. if command.Args[0].Lit() == "add-apt-repository" {
  77. return command.Args[len(command.Args)-1].Lit(), nil
  78. // Echoing into a sources file directly
  79. } else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) {
  80. for _, argument := range command.Args {
  81. var argumentNode syntax.Node = argument
  82. argumentValue := trimQuotes(c.shellNodeToString(argumentNode))
  83. if strings.Contains(argumentValue, "deb") || strings.Contains(argumentValue, "http://") {
  84. return argumentValue, nil
  85. }
  86. }
  87. }
  88. return "", errors.New("failed to parse repository")
  89. }
  90. // GetRepositories Returns a list of Repository entries that were identified
  91. func (c DebianCommandParser) GetRepositories(command string) ([]*Repository, error) {
  92. var repositories []*Repository
  93. reader := strings.NewReader(command)
  94. parsedScript, err := c.parser.Parse(reader, "")
  95. if err != nil {
  96. log.Fatalf("Failed to parse script \"%s\"", command)
  97. return nil, err
  98. }
  99. syntax.Walk(parsedScript, func(node syntax.Node) bool {
  100. switch parsedStatement := node.(type) {
  101. case *syntax.Stmt:
  102. switch parsedCommand := parsedStatement.Cmd.(type) {
  103. case *syntax.CallExpr:
  104. if isAddRepositoryExpression(*parsedCommand, parsedStatement.Redirs) {
  105. repositoryInformation, err := c.getAddedRepository(*parsedCommand, parsedStatement.Redirs)
  106. if err != nil {
  107. log.Fatalf("Unexpected error while parsing repository from \"%s\": %s", c.shellNodeToString(node), err)
  108. }
  109. repositories = append(repositories, &Repository{Information: repositoryInformation})
  110. log.Debugf("Found repository \"%s\"", repositoryInformation)
  111. }
  112. }
  113. }
  114. return true
  115. })
  116. return repositories, nil
  117. }
  118. // isAddRepositoryExpression Checks for expressions that add to the repository
  119. func isInstallerExpression(command syntax.CallExpr) bool {
  120. // Adding via debian tooling
  121. commandProgram := command.Args[0].Lit()
  122. if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" {
  123. return true
  124. }
  125. return false
  126. }
  127. func (c DebianCommandParser) getInstalledPinnedPackages(command syntax.CallExpr) ([]*Package, error) {
  128. var packages []*Package
  129. // Adding via debian tooling
  130. commandProgram := command.Args[0].Lit()
  131. if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" {
  132. for _, argument := range command.Args {
  133. var argumentNode syntax.Node = argument
  134. argumentValue := trimQuotes(c.shellNodeToString(argumentNode))
  135. if strings.Contains(argumentValue, "=") {
  136. pinnedPackageParts := strings.SplitN(argumentValue, "=", 2)
  137. packages = append(packages, &Package{
  138. Name: pinnedPackageParts[0],
  139. Version: pinnedPackageParts[1],
  140. })
  141. }
  142. }
  143. return packages, nil
  144. }
  145. return packages, errors.New("failed to parse packages")
  146. }
  147. // GetPinnedPackages get a list of all pinned packages
  148. func (c DebianCommandParser) GetPinnedPackages(command string) ([]*Package, error) {
  149. var packages []*Package
  150. reader := strings.NewReader(command)
  151. parsedScript, err := c.parser.Parse(reader, "")
  152. if err != nil {
  153. log.Fatalf("Failed to parse script \"%s\"", command)
  154. return nil, err
  155. }
  156. syntax.Walk(parsedScript, func(node syntax.Node) bool {
  157. switch parsedStatement := node.(type) {
  158. case *syntax.Stmt:
  159. switch parsedCommand := parsedStatement.Cmd.(type) {
  160. case *syntax.CallExpr:
  161. if isInstallerExpression(*parsedCommand) {
  162. pinnedPackages, err := c.getInstalledPinnedPackages(*parsedCommand)
  163. if err != nil {
  164. log.Fatalf("Unexpected error while parsing pinned packages from \"%s\": %s", c.shellNodeToString(node), err)
  165. }
  166. for _, pinnedPackage := range pinnedPackages {
  167. packages = append(packages, pinnedPackage)
  168. log.Debugf("Found pinned package \"%s=%s\"", pinnedPackage.Name, pinnedPackage.Version)
  169. }
  170. }
  171. }
  172. }
  173. return true
  174. })
  175. return packages, nil
  176. }