diff --git a/ale_linters/terraform/trivy.vim b/ale_linters/terraform/trivy.vim new file mode 100644 index 0000000000..58c6669a4e --- /dev/null +++ b/ale_linters/terraform/trivy.vim @@ -0,0 +1,92 @@ +" Description: trivy for Terraform files +" +" See: https://www.terraform.io/ +" https://github.com/aquasecurity/trivy + +call ale#Set('terraform_trivy_options', '') +call ale#Set('terraform_trivy_executable', 'trivy') + +function! ale_linters#terraform#trivy#Handle(buffer, lines) abort + let l:output = [] + let l:json = ale#util#FuzzyJSONDecode(a:lines, {}) + + if empty(get(l:json, 'Results')) + return l:output + endif + + let l:fname = expand('#' . a:buffer . ':t') + + for l:result in get(l:json, 'Results', []) + for l:misconfig in get(l:result, 'Misconfigurations', []) + let l:severity = get(l:misconfig, 'Severity', 'MEDIUM') + + if l:severity is# 'LOW' + let l:type = 'I' + elseif l:severity is# 'CRITICAL' || l:severity is# 'HIGH' + let l:type = 'E' + else + let l:type = 'W' + endif + + let l:cause = get(l:misconfig, 'CauseMetadata', {}) + let l:title = get(l:misconfig, 'Title', '') + let l:id = get(l:misconfig, 'ID', '') + let l:desc = get(l:misconfig, 'Description', '') + + " Module findings store the location in the caller's file + " in CauseMetadata.Occurrences. Use those when available. + let l:occurrences = get(l:cause, 'Occurrences', []) + + if !empty(l:occurrences) + for l:occurrence in l:occurrences + if get(l:occurrence, 'Filename', '') ==# l:fname + let l:loc = get(l:occurrence, 'Location', {}) + + call add(l:output, { + \ 'lnum': get(l:loc, 'StartLine', 1), + \ 'end_lnum': get(l:loc, 'EndLine', get(l:loc, 'StartLine', 1)), + \ 'text': l:title . ' [' . l:id . ']', + \ 'detail': l:id . ': ' . l:title . "\n" . l:desc, + \ 'code': l:id, + \ 'type': l:type, + \}) + endif + endfor + elseif get(l:result, 'Target', '') ==# l:fname + call add(l:output, { + \ 'lnum': get(l:cause, 'StartLine', 1), + \ 'end_lnum': get(l:cause, 'EndLine', get(l:cause, 'StartLine', 1)), + \ 'text': l:title . ' [' . l:id . ']', + \ 'detail': l:id . ': ' . l:title . "\n" . l:desc, + \ 'code': l:id, + \ 'type': l:type, + \}) + endif + endfor + endfor + + return l:output +endfunction + +" Construct command arguments to trivy with `terraform_trivy_options`. +function! ale_linters#terraform#trivy#GetCommand(buffer) abort + let l:cmd = '%e config --format json' + + let l:opts = ale#Var(a:buffer, 'terraform_trivy_options') + + if !empty(l:opts) + let l:cmd .= ' ' . l:opts + endif + + let l:cmd .= ' .' + + return l:cmd +endfunction + +call ale#linter#Define('terraform', { +\ 'name': 'trivy', +\ 'executable': {b -> ale#Var(b, 'terraform_trivy_executable')}, +\ 'cwd': '%s:h', +\ 'command': function('ale_linters#terraform#trivy#GetCommand'), +\ 'callback': 'ale_linters#terraform#trivy#Handle', +\}) diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index 97cec81448..6fba90dbe8 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -693,6 +693,7 @@ Notes: * `terraform-lsp` * `tflint` * `tfsec` + * `trivy` * Texinfo * `alex` * `cspell` diff --git a/doc/ale-terraform.txt b/doc/ale-terraform.txt index ae8a7bc5b8..a04088335b 100644 --- a/doc/ale-terraform.txt +++ b/doc/ale-terraform.txt @@ -139,6 +139,30 @@ g:ale_terraform_tflint_options to include '-f json' in your new value. +=============================================================================== +trivy *ale-terraform-trivy* + + *ale-options.terraform_trivy_executable* + *g:ale_terraform_trivy_executable* + *b:ale_terraform_trivy_executable* +terraform_trivy_executable +g:ale_terraform_trivy_executable + Type: |String| + Default: `'trivy'` + + This variable can be changed to use a different executable for trivy. + + *ale-options.terraform_trivy_options* + *g:ale_terraform_trivy_options* + *b:ale_terraform_trivy_options* +terraform_trivy_options +g:ale_terraform_trivy_options + Type: |String| + Default: `''` + + This variable can be changed to pass custom CLI flags to trivy. + + =============================================================================== tfsec *ale-terraform-tfsec* diff --git a/doc/ale.txt b/doc/ale.txt index 361dc75e03..91c2df3b75 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -4002,6 +4002,7 @@ documented in additional help files. terraform-ls..........................|ale-terraform-terraform-ls| terraform-lsp.........................|ale-terraform-terraform-lsp| tflint................................|ale-terraform-tflint| + trivy.................................|ale-terraform-trivy| tfsec.................................|ale-terraform-tfsec| tex.....................................|ale-tex-options| chktex................................|ale-tex-chktex| diff --git a/supported-tools.md b/supported-tools.md index 7a077ff878..2413ab5cdc 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -703,6 +703,7 @@ formatting. * [terraform-lsp](https://github.com/juliosueiras/terraform-lsp) :speech_balloon: * [tflint](https://github.com/wata727/tflint) * [tfsec](https://github.com/aquasecurity/tfsec) + * [trivy](https://github.com/aquasecurity/trivy) * Texinfo * [alex](https://github.com/get-alex/alex) * [cspell](https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell) diff --git a/test/handler/test_trivy_handler.vader b/test/handler/test_trivy_handler.vader new file mode 100644 index 0000000000..db44be148b --- /dev/null +++ b/test/handler/test_trivy_handler.vader @@ -0,0 +1,174 @@ +Before: + runtime ale_linters/terraform/trivy.vim + +After: + call ale#linter#Reset() + +Execute(The trivy handler should handle empty output): + AssertEqual + \ [], + \ ale_linters#terraform#trivy#Handle(bufnr(''), ['{}']) + +Execute(The trivy handler should handle null Results): + AssertEqual + \ [], + \ ale_linters#terraform#trivy#Handle(bufnr(''), ['{"Results": null}']) + +Execute(The trivy handler should parse direct findings correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 3, + \ 'end_lnum': 6, + \ 'text': 'Bucket does not have encryption enabled [AVD-AWS-0088]', + \ 'detail': "AVD-AWS-0088: Bucket does not have encryption enabled\n" + \ . 'S3 Buckets should be encrypted to protect the data that is stored within them if access is compromised.', + \ 'code': 'AVD-AWS-0088', + \ 'type': 'E', + \ }, + \ ], + \ ale_linters#terraform#trivy#Handle(bufnr(''), [json_encode( + \ { + \ 'Results': [ + \ { + \ 'Target': expand('#' . bufnr('') . ':t'), + \ 'Misconfigurations': [ + \ { + \ 'ID': 'AVD-AWS-0088', + \ 'Title': 'Bucket does not have encryption enabled', + \ 'Description': 'S3 Buckets should be encrypted to protect the data that is stored within them if access is compromised.', + \ 'Severity': 'HIGH', + \ 'CauseMetadata': { + \ 'StartLine': 3, + \ 'EndLine': 6, + \ }, + \ }, + \ ], + \ }, + \ ], + \ } + \ )]) + +Execute(The trivy handler should use Occurrences for module findings): + AssertEqual + \ [ + \ { + \ 'lnum': 12, + \ 'end_lnum': 17, + \ 'text': 'S3 Bucket Logging [AWS-0089]', + \ 'detail': "AWS-0089: S3 Bucket Logging\n" + \ . 'Ensures S3 bucket logging is enabled for S3 buckets', + \ 'code': 'AWS-0089', + \ 'type': 'I', + \ }, + \ ], + \ ale_linters#terraform#trivy#Handle(bufnr(''), [json_encode( + \ { + \ 'Results': [ + \ { + \ 'Target': 'git::ssh:/git@github.com/example/terraform-module?ref=v1.0.0/main.tf', + \ 'Misconfigurations': [ + \ { + \ 'ID': 'AWS-0089', + \ 'Title': 'S3 Bucket Logging', + \ 'Description': 'Ensures S3 bucket logging is enabled for S3 buckets', + \ 'Severity': 'LOW', + \ 'CauseMetadata': { + \ 'StartLine': 24, + \ 'EndLine': 33, + \ 'Occurrences': [ + \ { + \ 'Filename': expand('#' . bufnr('') . ':t'), + \ 'Location': { + \ 'StartLine': 12, + \ 'EndLine': 17, + \ }, + \ }, + \ ], + \ }, + \ }, + \ ], + \ }, + \ ], + \ } + \ )]) + +Execute(The trivy handler should skip module findings with no matching Occurrences): + AssertEqual + \ [], + \ ale_linters#terraform#trivy#Handle(bufnr(''), [json_encode( + \ { + \ 'Results': [ + \ { + \ 'Target': 'git::ssh:/git@github.com/example/terraform-module?ref=v1.0.0/main.tf', + \ 'Misconfigurations': [ + \ { + \ 'ID': 'AWS-0089', + \ 'Title': 'S3 Bucket Logging', + \ 'Description': 'Ensures S3 bucket logging is enabled', + \ 'Severity': 'LOW', + \ 'CauseMetadata': { + \ 'StartLine': 24, + \ 'EndLine': 33, + \ 'Occurrences': [ + \ { + \ 'Filename': 'other.tf', + \ 'Location': { + \ 'StartLine': 5, + \ 'EndLine': 10, + \ }, + \ }, + \ ], + \ }, + \ }, + \ ], + \ }, + \ ], + \ } + \ )]) + +Execute(The trivy handler should map severity levels correctly): + let g:result = ale_linters#terraform#trivy#Handle(bufnr(''), [json_encode( + \ { + \ 'Results': [ + \ { + \ 'Target': expand('#' . bufnr('') . ':t'), + \ 'Misconfigurations': [ + \ { + \ 'ID': 'AVD-001', + \ 'Title': 'Low issue', + \ 'Description': 'Low', + \ 'Severity': 'LOW', + \ 'CauseMetadata': {'StartLine': 1, 'EndLine': 1}, + \ }, + \ { + \ 'ID': 'AVD-002', + \ 'Title': 'Medium issue', + \ 'Description': 'Medium', + \ 'Severity': 'MEDIUM', + \ 'CauseMetadata': {'StartLine': 2, 'EndLine': 2}, + \ }, + \ { + \ 'ID': 'AVD-003', + \ 'Title': 'High issue', + \ 'Description': 'High', + \ 'Severity': 'HIGH', + \ 'CauseMetadata': {'StartLine': 3, 'EndLine': 3}, + \ }, + \ { + \ 'ID': 'AVD-004', + \ 'Title': 'Critical issue', + \ 'Description': 'Critical', + \ 'Severity': 'CRITICAL', + \ 'CauseMetadata': {'StartLine': 4, 'EndLine': 4}, + \ }, + \ ], + \ }, + \ ], + \ } + \ )]) + + AssertEqual 'I', g:result[0].type + AssertEqual 'W', g:result[1].type + AssertEqual 'E', g:result[2].type + AssertEqual 'E', g:result[3].type diff --git a/test/linter/test_terraform_trivy.vader b/test/linter/test_terraform_trivy.vader new file mode 100644 index 0000000000..7b5a83f77e --- /dev/null +++ b/test/linter/test_terraform_trivy.vader @@ -0,0 +1,20 @@ +Before: + call ale#assert#SetUpLinterTest('terraform', 'trivy') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The default command should be correct): + AssertLinter 'trivy', ale#Escape('trivy') . ' config --format json .' + +Execute(The default executable should be configurable): + let b:ale_terraform_trivy_executable = '/usr/bin/trivy' + + AssertLinter '/usr/bin/trivy', ale#Escape('/usr/bin/trivy') . ' config --format json .' + +Execute(Overriding options should work): + let g:ale_terraform_trivy_executable = '/usr/local/bin/trivy' + let g:ale_terraform_trivy_options = '--severity HIGH,CRITICAL' + + AssertLinter '/usr/local/bin/trivy', + \ ale#Escape('/usr/local/bin/trivy') . ' config --format json --severity HIGH,CRITICAL .'