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 }