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

3 years ago
3 years ago
3 years ago
  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. // GetRepositories Returns a list of Repository entries that were identified
  27. func (c DebianCommandParser) GetRepositories(command string) ([]*Repository, error) {
  28. var repositories []*Repository
  29. reader := strings.NewReader(command)
  30. parsedScript, scriptParseErr := c.parser.Parse(reader, "")
  31. if scriptParseErr != nil {
  32. log.Fatalf("Failed to parse script \"%s\": %s", command, scriptParseErr)
  33. return nil, scriptParseErr
  34. }
  35. var hasErrorWhileWalkingScript bool = false
  36. var errorWhileWalkingScript error
  37. syntax.Walk(parsedScript, func(node syntax.Node) bool {
  38. switch parsedStatement := node.(type) {
  39. case *syntax.Stmt:
  40. switch parsedCommand := parsedStatement.Cmd.(type) {
  41. case *syntax.CallExpr:
  42. if isAddRepositoryExpression(*parsedCommand, parsedStatement.Redirs) {
  43. repositoryInformation, repositoryParseErr := c.getAddedRepository(*parsedCommand, parsedStatement.Redirs)
  44. if repositoryParseErr != nil {
  45. log.Fatalf("Unexpected error while parsing repository from \"%s\": %s", c.shellNodeToString(node), repositoryParseErr)
  46. hasErrorWhileWalkingScript = true
  47. errorWhileWalkingScript = repositoryParseErr
  48. // Terminate walking early so we can raise an error
  49. return false
  50. }
  51. repositories = append(repositories, &Repository{Information: repositoryInformation})
  52. log.Debugf("Found repository \"%s\"", repositoryInformation)
  53. }
  54. }
  55. }
  56. return true
  57. })
  58. if hasErrorWhileWalkingScript {
  59. return repositories, errorWhileWalkingScript
  60. }
  61. return repositories, nil
  62. }
  63. // GetPinnedPackages get a list of all pinned packages
  64. func (c DebianCommandParser) GetPinnedPackages(command string) ([]*Package, error) {
  65. var packages []*Package
  66. reader := strings.NewReader(command)
  67. parsedScript, scriptParseErr := c.parser.Parse(reader, "")
  68. if scriptParseErr != nil {
  69. log.Fatalf("Failed to parse script \"%s\"", command)
  70. return nil, scriptParseErr
  71. }
  72. var hasErrorWhileWalkingScript bool = false
  73. var errorWhileWalkingScript error
  74. syntax.Walk(parsedScript, func(node syntax.Node) bool {
  75. switch parsedStatement := node.(type) {
  76. case *syntax.Stmt:
  77. switch parsedCommand := parsedStatement.Cmd.(type) {
  78. case *syntax.CallExpr:
  79. if isInstallerExpression(*parsedCommand) {
  80. pinnedPackages, pinnedPackageParseErr := c.getInstalledPinnedPackages(*parsedCommand)
  81. if pinnedPackageParseErr != nil {
  82. log.Fatalf("Unexpected error while parsing pinned packages from \"%s\": %s", c.shellNodeToString(node), pinnedPackageParseErr)
  83. hasErrorWhileWalkingScript = true
  84. errorWhileWalkingScript = pinnedPackageParseErr
  85. // Terminate walking early so we can raise an error
  86. return false
  87. }
  88. for _, pinnedPackage := range pinnedPackages {
  89. packages = append(packages, pinnedPackage)
  90. log.Debugf("Found pinned package \"%s=%s\"", pinnedPackage.Name, pinnedPackage.Version)
  91. }
  92. }
  93. }
  94. }
  95. return true
  96. })
  97. if hasErrorWhileWalkingScript {
  98. return packages, errorWhileWalkingScript
  99. }
  100. return packages, nil
  101. }
  102. /*
  103. General Helpers
  104. */
  105. // newDebianCommandParser Provision a new instance of DebianCommandParser with required components
  106. func newDebianCommandParser() DebianCommandParser {
  107. return DebianCommandParser{
  108. parser: syntax.NewParser(),
  109. printer: syntax.NewPrinter(),
  110. }
  111. }
  112. // shellNodeToString Convert a node to the string representation
  113. func (c DebianCommandParser) shellNodeToString(node syntax.Node) string {
  114. buffer := bytes.NewBufferString("")
  115. err := c.printer.Print(buffer, node)
  116. if err != nil {
  117. log.Panic("Failed to convert node to string")
  118. }
  119. return buffer.String()
  120. }
  121. // trimQuotes return a string without surrounding matching quotes
  122. func trimQuotes(value string) string {
  123. if value[0] == '"' && value[len(value)-1] == '"' {
  124. return value[1 : len(value)-1]
  125. } else if value[0] == '\'' && value[len(value)-1] == '\'' {
  126. return value[1 : len(value)-1]
  127. }
  128. return value
  129. }
  130. /*
  131. Repository Helpers
  132. */
  133. // isRedirectedToAptSourcesList Checks if the list of redirects contains the apt sources list directory
  134. func isRedirectedToAptSourcesList(redirects []*syntax.Redirect) bool {
  135. if redirects != nil && len(redirects) > 0 {
  136. for _, redirect := range redirects {
  137. if strings.Contains(redirect.Word.Lit(), "/etc/apt/sources.list") {
  138. return true
  139. }
  140. }
  141. }
  142. return false
  143. }
  144. // isAddRepositoryExpression Checks for expressions that add to the repository
  145. func isAddRepositoryExpression(command syntax.CallExpr, redirects []*syntax.Redirect) bool {
  146. // Adding via debian tooling
  147. if command.Args[0].Lit() == "add-apt-repository" {
  148. return true
  149. // Echoing into a sources file directly
  150. } else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) {
  151. return true
  152. }
  153. return false
  154. }
  155. // getAddedRepository parse the repository information from the shell commands
  156. func (c DebianCommandParser) getAddedRepository(command syntax.CallExpr, redirects []*syntax.Redirect) (string, error) {
  157. // Adding via debian tooling
  158. if command.Args[0].Lit() == "add-apt-repository" {
  159. return command.Args[len(command.Args)-1].Lit(), nil
  160. // Echoing into a sources file directly
  161. } else if command.Args[0].Lit() == "echo" && isRedirectedToAptSourcesList(redirects) {
  162. for _, argument := range command.Args {
  163. var argumentNode syntax.Node = argument
  164. argumentValue := trimQuotes(c.shellNodeToString(argumentNode))
  165. if strings.Contains(argumentValue, "deb") || strings.Contains(argumentValue, "http://") {
  166. return argumentValue, nil
  167. }
  168. }
  169. }
  170. return "", errors.New("failed to parse repository")
  171. }
  172. // isAddRepositoryExpression Checks for expressions that add to the repository
  173. func isInstallerExpression(command syntax.CallExpr) bool {
  174. // Adding via debian tooling
  175. commandProgram := command.Args[0].Lit()
  176. if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" {
  177. return true
  178. }
  179. return false
  180. }
  181. /*
  182. Pinned Package Helpers
  183. */
  184. func (c DebianCommandParser) getInstalledPinnedPackages(command syntax.CallExpr) ([]*Package, error) {
  185. var packages []*Package
  186. // Adding via debian tooling
  187. commandProgram := command.Args[0].Lit()
  188. if (commandProgram == "apt-get" || commandProgram == "apt") && len(command.Args) > 1 && command.Args[1].Lit() == "install" {
  189. for _, argument := range command.Args {
  190. var argumentNode syntax.Node = argument
  191. argumentValue := trimQuotes(c.shellNodeToString(argumentNode))
  192. if strings.Contains(argumentValue, "=") {
  193. pinnedPackageParts := strings.SplitN(argumentValue, "=", 2)
  194. packages = append(packages, &Package{
  195. Name: pinnedPackageParts[0],
  196. Version: pinnedPackageParts[1],
  197. })
  198. }
  199. }
  200. return packages, nil
  201. }
  202. return packages, errors.New("failed to parse packages")
  203. }