Skip to content

Commit

Permalink
Allow capturing tfsec output as a report (#21155)
Browse files Browse the repository at this point in the history
`tfsec` allows you to redirect output to a file, but the pants backend
doesn't expose that
right now. This change allows people to set the argument `report_name`
to capture it.

Added an integration test, and also tested on our terraform monorepo.

---------

Co-authored-by: Daniel Goldman <[email protected]>
  • Loading branch information
purajit and lilatomic authored Jul 24, 2024
1 parent 26db7ea commit 8e4450f
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 10 deletions.
2 changes: 2 additions & 0 deletions docs/notes/2.23.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ The `tfsec` linter now works on all supported platforms without extra config.

`tfsec` versions are now provided in semver format, without "v" prefixes.

`tfsec` now allows capturing reports generated using the `report_name` config.

Sandboxes for the `experimental-deploy` deployment (the execution of `terraform apply`) can now be preserved with `--keep-sandboxes`.

Terraform Lockfiles now participate in the dependency graph. `--changed-since` will now include targets affected by the changed lockfile.
Expand Down
28 changes: 21 additions & 7 deletions src/python/pants/backend/terraform/lint/tfsec/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from pants.backend.terraform.lint.tfsec.tfsec import SkipTfSecField, TFSec, TfSecRequest
from pants.backend.terraform.target_types import TerraformModuleTarget
from pants.core.goals.lint import LintResult
from pants.core.goals.lint import REPORT_DIR, LintResult
from pants.core.util_rules import config_files
from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest
from pants.core.util_rules.external_tool import DownloadedExternalTool, ExternalToolRequest
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.fs import Digest, MergeDigests
from pants.engine.fs import CreateDigest, Digest, Directory, MergeDigests, RemovePrefix
from pants.engine.platform import Platform
from pants.engine.process import FallibleProcessResult, Process
from pants.engine.rules import Get, MultiGet, collect_rules, rule
Expand All @@ -17,11 +17,19 @@

@rule
async def run_tfsec(request: TfSecRequest.Batch, tfsec: TFSec, platform: Platform) -> LintResult:
downloaded_tfsec, sources, config_file, custom_checks = await MultiGet(
(
downloaded_tfsec,
sources,
config_file,
custom_checks,
report_directory,
) = await MultiGet(
Get(DownloadedExternalTool, ExternalToolRequest, tfsec.get_request(platform)),
Get(SourceFiles, SourceFilesRequest(fs.sources for fs in request.elements)),
Get(ConfigFiles, ConfigFilesRequest, tfsec.config_request()),
Get(ConfigFiles, ConfigFilesRequest, tfsec.custom_checks_request()),
# Ensure that the empty report dir exists.
Get(Digest, CreateDigest([Directory(REPORT_DIR)])),
)

input_digest = await Get(
Expand All @@ -32,32 +40,38 @@ async def run_tfsec(request: TfSecRequest.Batch, tfsec: TFSec, platform: Platfor
sources.snapshot.digest,
config_file.snapshot.digest,
custom_checks.snapshot.digest,
report_directory,
)
),
)

computed_args = []
if tfsec.config:
computed_args = [f"--config-file={tfsec.config}"]
computed_args.append(f"--config-file={tfsec.config}")
if tfsec.custom_check_dir:
computed_args = [f"--custom-check-dir={tfsec.custom_check_dir}"]
computed_args.append(f"--custom-check-dir={tfsec.custom_check_dir}")

if tfsec.report_name:
computed_args.append(f"--out={REPORT_DIR}/{tfsec.report_name}")

argv = [
downloaded_tfsec.exe,
*computed_args,
*tfsec.args,
]
process_result = await Get(
result = await Get(
FallibleProcessResult,
Process(
argv=argv,
input_digest=input_digest,
output_directories=(REPORT_DIR,),
description=f"Run tfsec on {pluralize(len(sources.files), 'file')}",
level=LogLevel.DEBUG,
),
)

return LintResult.create(request, process_result)
report = await Get(Digest, RemovePrefix(result.output_digest, REPORT_DIR))
return LintResult.create(request, result, report=report)


def rules():
Expand Down
13 changes: 12 additions & 1 deletion src/python/pants/backend/terraform/lint/tfsec/tfsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
from pants.core.util_rules.partitions import PartitionerType
from pants.engine.platform import Platform
from pants.engine.target import BoolField, Target
from pants.option.option_types import ArgsListOption, BoolOption, DirOption, FileOption, SkipOption
from pants.option.option_types import (
ArgsListOption,
BoolOption,
DirOption,
FileOption,
SkipOption,
StrOption,
)
from pants.util.strutil import softwrap

logger = logging.getLogger(__name__)
Expand All @@ -37,6 +44,10 @@ class TFSec(ExternalTool):

skip = SkipOption("lint")
args = ArgsListOption(example="--minimum-severity=MEDIUM")
report_name = StrOption(
default=None,
help="If specified, will redirect the output to a file(s) under dist/lint/terraform-tfsec/ with the given name",
)
config = FileOption(
default=None,
advanced=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
from pants.core.goals.lint import LintResult
from pants.core.util_rules import source_files
from pants.engine.internals.native_engine import Address
from pants.engine.internals.native_engine import EMPTY_DIGEST, Address
from pants.engine.rules import QueryRule
from pants.testutil.rule_runner import RuleRunner

Expand All @@ -39,7 +39,7 @@
"""


def test_run_tfsec():
def set_up_rule_runner(tfsec_args: list[str]) -> RuleRunner:
rule_runner = RuleRunner(
target_types=[TerraformModuleTarget, TerraformDeploymentTarget],
rules=[
Expand All @@ -55,6 +55,7 @@ def test_run_tfsec():
[
"--terraform-tfsec-args='--no-colour'",
"--terraform-tfsec-config=.tfsec_config.json", # the config dir is readable, but we're testing the extra setting
*tfsec_args,
]
)

Expand All @@ -79,6 +80,12 @@ def test_run_tfsec():
}
)

return rule_runner


def test_run_tfsec():
rule_runner = set_up_rule_runner([])

target = rule_runner.get_target(Address("", target_name="good"))

result = rule_runner.request(
Expand All @@ -94,3 +101,28 @@ def test_run_tfsec():
assert (
TFSEC_CUSTOM_ERROR_CODE.lower() in result.stdout
), "Custom check code wasn't found in output, did we pull in our custom config (all files in .tfsec folder)?"


def test_run_tfsec_with_report():
rule_runner = set_up_rule_runner(
[
"--terraform-tfsec-report-name=tfsec.txt",
]
)

target = rule_runner.get_target(Address("", target_name="good"))

result = rule_runner.request(
LintResult,
[TfSecRequest.Batch("tfsec", (TerraformFieldSet.create(target),), PartitionMetadata(""))],
)

assert result.exit_code == 1
assert (
"1 file(s) written: reports/tfsec.txt" in result.stderr
), "No file was written, are extra args being passed?"
assert result.report != EMPTY_DIGEST
assert "1 ignored" in result.stdout, "Error wasn't ignored, did we pull in the config file?"
assert (
"\x1b[1m" not in result.stdout
), "Found colour control code in ouput, are extra-args being passed?"

0 comments on commit 8e4450f

Please sign in to comment.