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.

245 lines
7.5 KiB

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