github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/dockerfile/scanner_test.go (about)

     1  package dockerfile
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"testing"
     7  
     8  	"github.com/aquasecurity/defsec/pkg/framework"
     9  	"github.com/aquasecurity/defsec/pkg/rego"
    10  	"github.com/aquasecurity/defsec/pkg/rego/schemas"
    11  	"github.com/aquasecurity/defsec/pkg/scan"
    12  	"github.com/aquasecurity/defsec/pkg/scanners/options"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/aquasecurity/trivy-iac/test/testutil"
    17  )
    18  
    19  const DS006PolicyWithDockerfileSchema = `# METADATA
    20  # title: "COPY '--from' referring to the current image"
    21  # description: "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself."
    22  # scope: package
    23  # schemas:
    24  # - input: schema["dockerfile"]
    25  # related_resources:
    26  # - https://docs.docker.com/develop/develop-images/multistage-build/
    27  # custom:
    28  #   id: DS006
    29  #   avd_id: AVD-DS-0006
    30  #   severity: CRITICAL
    31  #   short_code: no-self-referencing-copy-from
    32  #   recommended_action: "Change the '--from' so that it will not refer to itself"
    33  #   input:
    34  #     selector:
    35  #     - type: dockerfile
    36  package builtin.dockerfile.DS006
    37  
    38  import data.lib.docker
    39  
    40  get_alias_from_copy[output] {
    41  	copies := docker.stage_copies[stage]
    42  
    43  	copy := copies[_]
    44  	flag := copy.Flags[_]
    45  	contains(flag, "--from=")
    46  	parts := split(flag, "=")
    47  
    48  	is_alias_current_from_alias(stage.Name, parts[1])
    49  	args := parts[1]
    50  	output := {
    51  		"args": args,
    52  		"cmd": copy,
    53  	}
    54  }
    55  
    56  is_alias_current_from_alias(current_name, current_alias) = allow {
    57  	current_name_lower := lower(current_name)
    58  	current_alias_lower := lower(current_alias)
    59  
    60  	#expecting stage name as "myimage:tag as dep"
    61  	[_, alias] := regex.split(` + "`\\s+as\\s+`" + `, current_name_lower)
    62  
    63  	alias == current_alias
    64  
    65  	allow = true
    66  }
    67  
    68  deny[res] {
    69  	output := get_alias_from_copy[_]
    70  	msg := sprintf("'COPY --from' should not mention current alias '%s' since it is impossible to copy from itself", [output.args])
    71  	res := result.new(msg, output.cmd)
    72  }
    73  `
    74  
    75  const DS006PolicyWithMyFancyDockerfileSchema = `# METADATA
    76  # title: "COPY '--from' referring to the current image"
    77  # description: "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself."
    78  # scope: package
    79  # schemas:
    80  # - input: schema["myfancydockerfile"]
    81  # related_resources:
    82  # - https://docs.docker.com/develop/develop-images/multistage-build/
    83  # custom:
    84  #   id: DS006
    85  #   avd_id: AVD-DS-0006
    86  #   severity: CRITICAL
    87  #   short_code: no-self-referencing-copy-from
    88  #   recommended_action: "Change the '--from' so that it will not refer to itself"
    89  #   input:
    90  #     selector:
    91  #     - type: dockerfile
    92  package builtin.dockerfile.DS006
    93  
    94  import data.lib.docker
    95  
    96  get_alias_from_copy[output] {
    97  copies := docker.stage_copies[stage]
    98  
    99  copy := copies[_]
   100  flag := copy.Flags[_]
   101  contains(flag, "--from=")
   102  parts := split(flag, "=")
   103  
   104  is_alias_current_from_alias(stage.Name, parts[1])
   105  args := parts[1]
   106  output := {
   107  "args": args,
   108  "cmd": copy,
   109  }
   110  }
   111  
   112  is_alias_current_from_alias(current_name, current_alias) = allow {
   113  current_name_lower := lower(current_name)
   114  current_alias_lower := lower(current_alias)
   115  
   116  #expecting stage name as "myimage:tag as dep"
   117  [_, alias] := regex.split(` + "`\\s+as\\s+`" + `, current_name_lower)
   118  
   119  alias == current_alias
   120  
   121  allow = true
   122  }
   123  
   124  deny[res] {
   125  output := get_alias_from_copy[_]
   126  msg := sprintf("'COPY --from' should not mention current alias '%s' since it is impossible to copy from itself", [output.args])
   127  res := result.new(msg, output.cmd)
   128  }
   129  `
   130  
   131  const DS006PolicyWithOldSchemaSelector = `# METADATA
   132  # title: "COPY '--from' referring to the current image"
   133  # description: "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself."
   134  # scope: package
   135  # schemas:
   136  # - input: schema["input"]
   137  # related_resources:
   138  # - https://docs.docker.com/develop/develop-images/multistage-build/
   139  # custom:
   140  #   id: DS006
   141  #   avd_id: AVD-DS-0006
   142  #   severity: CRITICAL
   143  #   short_code: no-self-referencing-copy-from
   144  #   recommended_action: "Change the '--from' so that it will not refer to itself"
   145  #   input:
   146  #     selector:
   147  #     - type: dockerfile
   148  package builtin.dockerfile.DS006
   149  
   150  import data.lib.docker
   151  
   152  get_alias_from_copy[output] {
   153  	copies := docker.stage_copies[stage]
   154  
   155  	copy := copies[_]
   156  	flag := copy.Flags[_]
   157  	contains(flag, "--from=")
   158  	parts := split(flag, "=")
   159  
   160  	is_alias_current_from_alias(stage.Name, parts[1])
   161  	args := parts[1]
   162  	output := {
   163  		"args": args,
   164  		"cmd": copy,
   165  	}
   166  }
   167  
   168  is_alias_current_from_alias(current_name, current_alias) = allow {
   169  	current_name_lower := lower(current_name)
   170  	current_alias_lower := lower(current_alias)
   171  
   172  	#expecting stage name as "myimage:tag as dep"
   173  	[_, alias] := regex.split(` + "`\\s+as\\s+`" + `, current_name_lower)
   174  
   175  	alias == current_alias
   176  
   177  	allow = true
   178  }
   179  
   180  deny[res] {
   181  	output := get_alias_from_copy[_]
   182  	msg := sprintf("'COPY --from' should not mention current alias '%s' since it is impossible to copy from itself", [output.args])
   183  	res := result.new(msg, output.cmd)
   184  }
   185  `
   186  const DS006LegacyWithOldStyleMetadata = `package builtin.dockerfile.DS006
   187  
   188  __rego_metadata__ := {
   189  	"id": "DS006",
   190  	"avd_id": "AVD-DS-0006",
   191  	"title": "COPY '--from' referring to the current image",
   192  	"short_code": "no-self-referencing-copy-from",
   193  	"version": "v1.0.0",
   194  	"severity": "CRITICAL",
   195  	"type": "Dockerfile Security Check",
   196  	"description": "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself.",
   197  	"recommended_actions": "Change the '--from' so that it will not refer to itself",
   198  	"url": "https://docs.docker.com/develop/develop-images/multistage-build/",
   199  }
   200  
   201  __rego_input__ := {
   202  	"combine": false,
   203  	"selector": [{"type": "dockerfile"}],
   204  }
   205  
   206  deny[res] {
   207  	res := {
   208  		"msg": "oh no",
   209  		"filepath": "code/Dockerfile",
   210  		"startline": 1,
   211  		"endline": 1,
   212  	}
   213  }`
   214  
   215  func Test_BasicScanLegacyRegoMetadata(t *testing.T) {
   216  	fs := testutil.CreateFS(t, map[string]string{
   217  		"/code/Dockerfile": `FROM ubuntu
   218  USER root
   219  `,
   220  		"/rules/rule.rego": DS006LegacyWithOldStyleMetadata,
   221  	})
   222  
   223  	scanner := NewScanner(options.ScannerWithPolicyDirs("rules"))
   224  
   225  	results, err := scanner.ScanFS(context.TODO(), fs, "code")
   226  	require.NoError(t, err)
   227  
   228  	require.Len(t, results.GetFailed(), 1)
   229  
   230  	failure := results.GetFailed()[0]
   231  	metadata := failure.Metadata()
   232  	assert.Equal(t, 1, metadata.Range().GetStartLine())
   233  	assert.Equal(t, 1, metadata.Range().GetEndLine())
   234  	assert.Equal(t, "code/Dockerfile", metadata.Range().GetFilename())
   235  
   236  	assert.Equal(
   237  		t,
   238  		scan.Rule{
   239  			AVDID:          "AVD-DS-0006",
   240  			Aliases:        []string{"DS006"},
   241  			ShortCode:      "no-self-referencing-copy-from",
   242  			Summary:        "COPY '--from' referring to the current image",
   243  			Explanation:    "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself.",
   244  			Impact:         "",
   245  			Resolution:     "Change the '--from' so that it will not refer to itself",
   246  			Provider:       "dockerfile",
   247  			Service:        "general",
   248  			Links:          []string{"https://docs.docker.com/develop/develop-images/multistage-build/"},
   249  			Severity:       "CRITICAL",
   250  			Terraform:      &scan.EngineMetadata{},
   251  			CloudFormation: &scan.EngineMetadata{},
   252  			CustomChecks: scan.CustomChecks{
   253  				Terraform: (*scan.TerraformCustomCheck)(nil)},
   254  			RegoPackage: "data.builtin.dockerfile.DS006",
   255  			Frameworks:  map[framework.Framework][]string{},
   256  		},
   257  		results.GetFailed()[0].Rule(),
   258  	)
   259  
   260  	actualCode, err := results.GetFailed()[0].GetCode()
   261  	require.NoError(t, err)
   262  	for i := range actualCode.Lines {
   263  		actualCode.Lines[i].Highlighted = ""
   264  	}
   265  	assert.Equal(t, []scan.Line{
   266  		{
   267  			Number:     1,
   268  			Content:    "FROM ubuntu",
   269  			IsCause:    true,
   270  			FirstCause: true,
   271  			LastCause:  true,
   272  			Annotation: "",
   273  		},
   274  	}, actualCode.Lines)
   275  }
   276  
   277  func Test_BasicScanNewRegoMetadata(t *testing.T) {
   278  	var testCases = []struct {
   279  		name                    string
   280  		inputRegoPolicy         string
   281  		expectedError           string
   282  		expectedInputTraceLogs  string
   283  		expectedOutputTraceLogs string
   284  	}{
   285  		{
   286  			name:            "old schema selector schema.input",
   287  			inputRegoPolicy: DS006PolicyWithOldSchemaSelector,
   288  			expectedInputTraceLogs: `REGO INPUT:
   289  {
   290    "path": "code/Dockerfile",
   291    "contents": {
   292      "Stages": [
   293        {
   294          "Commands": [
   295            {
   296              "Cmd": "from",
   297              "EndLine": 1,
   298              "Flags": [],
   299              "JSON": false,
   300              "Original": "FROM golang:1.7.3 as dep",
   301              "Path": "code/Dockerfile",
   302              "Stage": 0,
   303              "StartLine": 1,
   304              "SubCmd": "",
   305              "Value": [
   306                "golang:1.7.3",
   307                "as",
   308                "dep"
   309              ]
   310            },
   311            {
   312              "Cmd": "copy",
   313              "EndLine": 2,
   314              "Flags": [
   315                "--from=dep"
   316              ],
   317              "JSON": false,
   318              "Original": "COPY --from=dep /binary /",
   319              "Path": "code/Dockerfile",
   320              "Stage": 0,
   321              "StartLine": 2,
   322              "SubCmd": "",
   323              "Value": [
   324                "/binary",
   325                "/"
   326              ]
   327            }
   328          ],
   329          "Name": "golang:1.7.3 as dep"
   330        }
   331      ]
   332    }
   333  }
   334  END REGO INPUT
   335  `,
   336  			expectedOutputTraceLogs: `REGO RESULTSET:
   337  [
   338    {
   339      "expressions": [
   340        {
   341          "value": [
   342            {
   343              "endline": 2,
   344              "explicit": false,
   345              "filepath": "code/Dockerfile",
   346              "fskey": "",
   347              "managed": true,
   348              "msg": "'COPY --from' should not mention current alias 'dep' since it is impossible to copy from itself",
   349              "parent": null,
   350              "resource": "",
   351              "sourceprefix": "",
   352              "startline": 2
   353            }
   354          ],
   355          "text": "data.builtin.dockerfile.DS006.deny",
   356          "location": {
   357            "row": 1,
   358            "col": 1
   359          }
   360        }
   361      ]
   362    }
   363  ]
   364  END REGO RESULTSET
   365  
   366  `,
   367  		},
   368  		{
   369  			name:            "new schema selector schema.dockerfile",
   370  			inputRegoPolicy: DS006PolicyWithDockerfileSchema,
   371  			expectedInputTraceLogs: `REGO INPUT:
   372  {
   373    "path": "code/Dockerfile",
   374    "contents": {
   375      "Stages": [
   376        {
   377          "Commands": [
   378            {
   379              "Cmd": "from",
   380              "EndLine": 1,
   381              "Flags": [],
   382              "JSON": false,
   383              "Original": "FROM golang:1.7.3 as dep",
   384              "Path": "code/Dockerfile",
   385              "Stage": 0,
   386              "StartLine": 1,
   387              "SubCmd": "",
   388              "Value": [
   389                "golang:1.7.3",
   390                "as",
   391                "dep"
   392              ]
   393            },
   394            {
   395              "Cmd": "copy",
   396              "EndLine": 2,
   397              "Flags": [
   398                "--from=dep"
   399              ],
   400              "JSON": false,
   401              "Original": "COPY --from=dep /binary /",
   402              "Path": "code/Dockerfile",
   403              "Stage": 0,
   404              "StartLine": 2,
   405              "SubCmd": "",
   406              "Value": [
   407                "/binary",
   408                "/"
   409              ]
   410            }
   411          ],
   412          "Name": "golang:1.7.3 as dep"
   413        }
   414      ]
   415    }
   416  }
   417  END REGO INPUT
   418  `,
   419  			expectedOutputTraceLogs: `REGO RESULTSET:
   420  [
   421    {
   422      "expressions": [
   423        {
   424          "value": [
   425            {
   426              "endline": 2,
   427              "explicit": false,
   428              "filepath": "code/Dockerfile",
   429              "fskey": "",
   430              "managed": true,
   431              "msg": "'COPY --from' should not mention current alias 'dep' since it is impossible to copy from itself",
   432              "parent": null,
   433              "resource": "",
   434              "sourceprefix": "",
   435              "startline": 2
   436            }
   437          ],
   438          "text": "data.builtin.dockerfile.DS006.deny",
   439          "location": {
   440            "row": 1,
   441            "col": 1
   442          }
   443        }
   444      ]
   445    }
   446  ]
   447  END REGO RESULTSET
   448  
   449  `,
   450  		},
   451  		{
   452  			name:            "new schema selector with custom schema.myfancydockerfile",
   453  			inputRegoPolicy: DS006PolicyWithMyFancyDockerfileSchema,
   454  			expectedInputTraceLogs: `REGO INPUT:
   455  {
   456    "path": "code/Dockerfile",
   457    "contents": {
   458      "Stages": [
   459        {
   460          "Commands": [
   461            {
   462              "Cmd": "from",
   463              "EndLine": 1,
   464              "Flags": [],
   465              "JSON": false,
   466              "Original": "FROM golang:1.7.3 as dep",
   467              "Path": "code/Dockerfile",
   468              "Stage": 0,
   469              "StartLine": 1,
   470              "SubCmd": "",
   471              "Value": [
   472                "golang:1.7.3",
   473                "as",
   474                "dep"
   475              ]
   476            },
   477            {
   478              "Cmd": "copy",
   479              "EndLine": 2,
   480              "Flags": [
   481                "--from=dep"
   482              ],
   483              "JSON": false,
   484              "Original": "COPY --from=dep /binary /",
   485              "Path": "code/Dockerfile",
   486              "Stage": 0,
   487              "StartLine": 2,
   488              "SubCmd": "",
   489              "Value": [
   490                "/binary",
   491                "/"
   492              ]
   493            }
   494          ],
   495          "Name": "golang:1.7.3 as dep"
   496        }
   497      ]
   498    }
   499  }
   500  END REGO INPUT
   501  `,
   502  			expectedOutputTraceLogs: `REGO RESULTSET:
   503  [
   504    {
   505      "expressions": [
   506        {
   507          "value": [
   508            {
   509              "endline": 2,
   510              "explicit": false,
   511              "filepath": "code/Dockerfile",
   512              "fskey": "",
   513              "managed": true,
   514              "msg": "'COPY --from' should not mention current alias 'dep' since it is impossible to copy from itself",
   515              "parent": null,
   516              "resource": "",
   517              "sourceprefix": "",
   518              "startline": 2
   519            }
   520          ],
   521          "text": "data.builtin.dockerfile.DS006.deny",
   522          "location": {
   523            "row": 1,
   524            "col": 1
   525          }
   526        }
   527      ]
   528    }
   529  ]
   530  END REGO RESULTSET
   531  
   532  `,
   533  		},
   534  		{
   535  			name: "new schema selector but invalid",
   536  			inputRegoPolicy: `# METADATA
   537  # title: "COPY '--from' referring to the current image"
   538  # description: "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself."
   539  # scope: package
   540  # schemas:
   541  # - input: schema["spooky-schema"]
   542  # custom:
   543  #   input:
   544  #     selector:
   545  #     - type: dockerfile
   546  package builtin.dockerfile.DS006
   547  deny[res]{
   548  res := true
   549  }`,
   550  			expectedError: `1 error occurred: rules/rule.rego:12: rego_type_error: undefined schema: schema["spooky-schema"]`,
   551  		},
   552  	}
   553  
   554  	for _, tc := range testCases {
   555  		t.Run(tc.name, func(t *testing.T) {
   556  			regoMap := make(map[string]string)
   557  			libs, err := rego.LoadEmbeddedLibraries()
   558  			require.NoError(t, err)
   559  			for name, library := range libs {
   560  				regoMap["/rules/"+name] = library.String()
   561  			}
   562  			regoMap["/code/Dockerfile"] = `FROM golang:1.7.3 as dep
   563  COPY --from=dep /binary /`
   564  			regoMap["/rules/rule.rego"] = tc.inputRegoPolicy
   565  			regoMap["/rules/schemas/myfancydockerfile.json"] = string(schemas.Dockerfile) // just use the same for testing
   566  			fs := testutil.CreateFS(t, regoMap)
   567  
   568  			var traceBuf bytes.Buffer
   569  			var debugBuf bytes.Buffer
   570  
   571  			scanner := NewScanner(
   572  				options.ScannerWithPolicyDirs("rules"),
   573  				options.ScannerWithTrace(&traceBuf),
   574  				options.ScannerWithDebug(&debugBuf),
   575  				options.ScannerWithRegoErrorLimits(0),
   576  			)
   577  
   578  			results, err := scanner.ScanFS(context.TODO(), fs, "code")
   579  			if tc.expectedError != "" && err != nil {
   580  				require.Equal(t, tc.expectedError, err.Error(), tc.name)
   581  			} else {
   582  				require.NoError(t, err)
   583  				require.Len(t, results.GetFailed(), 1)
   584  
   585  				failure := results.GetFailed()[0]
   586  				metadata := failure.Metadata()
   587  				assert.Equal(t, 2, metadata.Range().GetStartLine())
   588  				assert.Equal(t, 2, metadata.Range().GetEndLine())
   589  				assert.Equal(t, "code/Dockerfile", metadata.Range().GetFilename())
   590  
   591  				assert.Equal(
   592  					t,
   593  					scan.Rule{
   594  						AVDID:          "AVD-DS-0006",
   595  						Aliases:        []string{"DS006"},
   596  						ShortCode:      "no-self-referencing-copy-from",
   597  						Summary:        "COPY '--from' referring to the current image",
   598  						Explanation:    "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself.",
   599  						Impact:         "",
   600  						Resolution:     "Change the '--from' so that it will not refer to itself",
   601  						Provider:       "dockerfile",
   602  						Service:        "general",
   603  						Links:          []string{"https://docs.docker.com/develop/develop-images/multistage-build/"},
   604  						Severity:       "CRITICAL",
   605  						Terraform:      &scan.EngineMetadata{},
   606  						CloudFormation: &scan.EngineMetadata{},
   607  						CustomChecks: scan.CustomChecks{
   608  							Terraform: (*scan.TerraformCustomCheck)(nil)},
   609  						RegoPackage: "data.builtin.dockerfile.DS006",
   610  						Frameworks:  map[framework.Framework][]string{},
   611  					},
   612  					results.GetFailed()[0].Rule(),
   613  				)
   614  
   615  				actualCode, err := results.GetFailed()[0].GetCode()
   616  				require.NoError(t, err)
   617  				for i := range actualCode.Lines {
   618  					actualCode.Lines[i].Highlighted = ""
   619  				}
   620  				assert.Equal(t, []scan.Line{
   621  					{
   622  						Number:     2,
   623  						Content:    "COPY --from=dep /binary /",
   624  						IsCause:    true,
   625  						FirstCause: true,
   626  						LastCause:  true,
   627  						Annotation: "",
   628  					},
   629  				}, actualCode.Lines)
   630  
   631  				// assert logs
   632  				assert.Contains(t, traceBuf.String(), tc.expectedInputTraceLogs, traceBuf.String())
   633  				assert.Contains(t, traceBuf.String(), tc.expectedOutputTraceLogs, traceBuf.String())
   634  			}
   635  		})
   636  	}
   637  
   638  }