diff --git a/README.md b/README.md
index 8a6f8b25d..e2a9019a5 100644
--- a/README.md
+++ b/README.md
@@ -29,4 +29,4 @@ Please follow our [Getting Started Guide](https://bancolombia.github.io/devsecop
# How can I help?
-Review the issues, we hear new ideas. Read more [Contributing](https://github.com/bancolombia/devsecops-engine-tools/blob/trunk/docs/CONTRIBUTING.md)
+Review the issues, we hear new ideas. Read more [Contributing](https://github.com/bancolombia/devsecops-engine-tools/blob/trunk/docs/CONTRIBUTING.md)
\ No newline at end of file
diff --git a/docs/Docusaurus/docs/life_cycle/getting_started.md b/docs/Docusaurus/docs/life_cycle/getting_started.md
index 6026d80e6..036bee46b 100644
--- a/docs/Docusaurus/docs/life_cycle/getting_started.md
+++ b/docs/Docusaurus/docs/life_cycle/getting_started.md
@@ -45,6 +45,10 @@ For more information about structure remote config visit [Structure Remote Confi
| KICS |
Free |
+
+
+ | CONFTEST |
+ Free |
| ENGINE_DAST |
diff --git a/docs/Docusaurus/docs/modules/engine_core.md b/docs/Docusaurus/docs/modules/engine_core.md
index a6dd7cefd..ea22448f1 100644
--- a/docs/Docusaurus/docs/modules/engine_core.md
+++ b/docs/Docusaurus/docs/modules/engine_core.md
@@ -210,7 +210,7 @@ Configuration of the driven adapters in the main layer and management of on/off
},
"ENGINE_IAC": {
"ENABLED": true,
- "TOOL": "CHECKOV|KUBESCAPE|KICS",
+ "TOOL": "CHECKOV|KUBESCAPE|KICS|CONFTEST",
"PRIORITY": "STANDARD|DISCREET"
},
"ENGINE_CONTAINER": {
diff --git a/docs/Docusaurus/docs/modules/engine_sast/engine_iac.md b/docs/Docusaurus/docs/modules/engine_sast/engine_iac.md
index 90438dd44..3e84a784d 100755
--- a/docs/Docusaurus/docs/modules/engine_sast/engine_iac.md
+++ b/docs/Docusaurus/docs/modules/engine_sast/engine_iac.md
@@ -170,6 +170,42 @@ Main configuration file that defines scanning behavior, tool versions, and secur
},
"KUBESCAPE": {
"VERSION": "3.0.11"
+ },
+ "CONFTEST": {
+ "VERSION": "0.56.0",
+ "POLICY_PATH": "policy",
+ "USE_EXTERNAL_CHECKS_DIR": false,
+ "EXTERNAL_DIR_OWNER": "",
+ "EXTERNAL_DIR_REPOSITORY": "",
+ "EXTERNAL_DIR_POLICIES_PATH": "policy",
+ "APP_ID_GITHUB": "",
+ "INSTALLATION_ID_GITHUB": "",
+ "DEFAULT_SEVERITY": "high",
+ "DEFAULT_CATEGORY": "vulnerability",
+ "RULES": {
+ "RULES_N8N": {
+ "CONF_N8N_BC_1": {
+ "severity": "Critical",
+ "category": "Vulnerability"
+ },
+ "CONF_N8N_BC_2": {
+ "severity": "Medium",
+ "category": "Vulnerability"
+ },
+ "CONF_N8N_BC_3": {
+ "severity": "High",
+ "category": "Vulnerability"
+ },
+ "CONF_N8N_BC_4": {
+ "severity": "Low",
+ "category": "Compliance"
+ },
+ "CONF_N8N_BC_5": {
+ "severity": "Critical",
+ "category": "Vulnerability"
+ }
+ }
+ }
}
}
```
@@ -232,6 +268,21 @@ Main configuration file that defines scanning behavior, tool versions, and secur
##### Kubescape Tool Configuration
- **VERSION**: Kubescape version to use (e.g., `"3.0.11"`)
+##### Conftest Tool Configuration
+- **VERSION**: Conftest version to download (e.g., `"0.56.0"`)
+- **POLICY_PATH**: Local path to the directory containing `.rego` policy files (default `"policy"`)
+- **External Policy Configuration** (via GitHub App):
+ - `USE_EXTERNAL_CHECKS_DIR`: Download policies from a GitHub repository (`true`/`false`)
+ - `EXTERNAL_DIR_OWNER`: GitHub organisation or user owning the policy repository
+ - `EXTERNAL_DIR_REPOSITORY`: Repository name containing the policies
+ - `EXTERNAL_DIR_POLICIES_PATH`: Path inside the repository where policies are stored
+ - `APP_ID_GITHUB`: GitHub App ID used for authentication
+ - `INSTALLATION_ID_GITHUB`: GitHub App installation ID
+- **Default Values**:
+ - `DEFAULT_SEVERITY`: Fallback severity when a rule has no entry in `RULES` (e.g., `"high"`)
+ - `DEFAULT_CATEGORY`: Fallback category when a rule has no entry in `RULES` (e.g., `"vulnerability"`)
+- **RULES**: Rule metadata keyed by platform group then rule ID (see [Security Rules Configuration](#security-rules-configuration) below)
+
##### Security Rules Configuration
Each tool contains rule sets organized by technology:
@@ -256,6 +307,7 @@ Each tool contains rule sets organized by technology:
- **RULES_OPENAPI**: API security rules for OpenAPI specifications
- **RULES_SERVERLESS**: Serverless rules for AWS Lambda specifications
- **RULES_BICEP**: Azure Bicep security rules for Azure Cloud infrastructure
+- **RULES_N8N**: n8n workflow security rules (authentication, transport security, dangerous nodes, error handling, hardcoded credentials)
In the RULES section of each platform (RULES_DOCKER, RULES_K8S, RULES_CLOUDFORMATION, etc.), the body is empty. Example “RULES_DOCKER” {}, the tool executes all rules associated with it.
@@ -350,7 +402,7 @@ Defines exclusion rules for repositories and specific security checks.
## Main Responsibilities
-- **IaC Security Orchestration:** Executes IaC security tools (Checkov, KICS, Kubescape) on infrastructure code
+- **IaC Security Orchestration:** Executes IaC security tools (Checkov, KICS, Kubescape, Conftest) on infrastructure code
- **Configuration Management:** Loads and processes scan configurations and exclusions from remote repositories
- **Folder and File Discovery:** Identifies relevant folders/files for scanning based on patterns and configuration
- **Exclusions Management:** Applies exclusion rules based on configuration and DevSecOps policy
@@ -363,13 +415,14 @@ Defines exclusion rules for repositories and specific security checks.
- `runner_iac_scan.py`: Main entry point for IaC scan orchestration
- `entry_point_tool.py`: Initializes the IaC engine and triggers the scan process
- `iac_scan.py`: Core use case for executing the scan, handling configuration, exclusions, and result aggregation
-- **Adapters:** Integrations for IaC security tools (Checkov, KICS, Kubescape)
+- **Adapters:** Integrations for IaC security tools (Checkov, KICS, Kubescape, Conftest)
## Supported Tools and Features
- **Checkov:** Scans Terraform, CloudFormation, Kubernetes, Docker, and more for security misconfigurations
- **KICS:** Scans IaC files for vulnerabilities and compliance issues with OpenAPI support
- **Kubescape:** Focused on Kubernetes security scanning with RBAC analysis
+- **Conftest:** Policy-as-code scanning using OPA/Rego policies; supports namespace-based platform filtering and GitHub-hosted policy repositories
- **Configurable Exclusions:** Supports exclusion of files/folders and custom ignore patterns with expiration dates
- **Thresholds and Policies:** Handles custom thresholds and build-breaking policies for vulnerabilities and compliance
- **Multi-platform Support:** Cross-platform binary distribution for Linux, macOS, and Windows
diff --git a/example_remote_config_local/engine_core/ConfigTool.json b/example_remote_config_local/engine_core/ConfigTool.json
index 21bbaab6b..2d395833c 100644
--- a/example_remote_config_local/engine_core/ConfigTool.json
+++ b/example_remote_config_local/engine_core/ConfigTool.json
@@ -189,7 +189,7 @@
},
"ENGINE_IAC": {
"ENABLED": true,
- "TOOL": "CHECKOV|KUBESCAPE|KICS",
+ "TOOL": "CHECKOV|KUBESCAPE|KICS|CONFTEST",
"PRIORITY": "STANDARD|DISCREET"
},
"ENGINE_CONTAINER": {
diff --git a/example_remote_config_local/engine_sast/engine_iac/ConfigTool.json b/example_remote_config_local/engine_sast/engine_iac/ConfigTool.json
index 01148a845..f8ae8624a 100755
--- a/example_remote_config_local/engine_sast/engine_iac/ConfigTool.json
+++ b/example_remote_config_local/engine_sast/engine_iac/ConfigTool.json
@@ -809,5 +809,46 @@
}
}
}
+ },
+ "CONFTEST": {
+ "VERSION": "0.56.0",
+ "POLICY_PATH": "policy",
+ "USE_EXTERNAL_CHECKS_DIR": false,
+ "EXTERNAL_DIR_OWNER": "",
+ "EXTERNAL_DIR_REPOSITORY": "",
+ "EXTERNAL_DIR_POLICIES_PATH": "policy",
+ "APP_ID_GITHUB": "",
+ "INSTALLATION_ID_GITHUB": "",
+ "DEFAULT_SEVERITY": "high",
+ "DEFAULT_CATEGORY": "vulnerability",
+ "RULES": {
+ "RULES_N8N": {
+ "CONF_N8N_BC_1": {
+ "checkID": "CONF_N8N_BC_1 Webhook node exposes an unauthenticated endpoint",
+ "severity": "Critical",
+ "category": "Vulnerability"
+ },
+ "CONF_N8N_BC_2": {
+ "checkID": "CONF_N8N_BC_2 HTTP Request node uses insecure HTTP",
+ "severity": "Medium",
+ "category": "Vulnerability"
+ },
+ "CONF_N8N_BC_3": {
+ "checkID": "CONF_N8N_BC_3 Execute Command node can introduce command-injection risks",
+ "severity": "High",
+ "category": "Vulnerability"
+ },
+ "CONF_N8N_BC_4": {
+ "checkID": "CONF_N8N_BC_4 Active workflow has no error-handling node",
+ "severity": "Low",
+ "category": "Compliance"
+ },
+ "CONF_N8N_BC_5": {
+ "checkID": "CONF_N8N_BC_5 Credential hardcoded in node parameter",
+ "severity": "Critical",
+ "category": "Vulnerability"
+ }
+ }
+ }
}
}
diff --git a/tools/devsecops_engine_tools/engine_core/src/applications/runner_engine_core.py b/tools/devsecops_engine_tools/engine_core/src/applications/runner_engine_core.py
index 64fa38c26..d8e3f3132 100755
--- a/tools/devsecops_engine_tools/engine_core/src/applications/runner_engine_core.py
+++ b/tools/devsecops_engine_tools/engine_core/src/applications/runner_engine_core.py
@@ -109,6 +109,7 @@ def get_inputs_from_cli(args):
"checkov",
"kics",
"kubescape",
+ "conftest",
"trufflehog",
"gitleaks",
"prisma",
@@ -156,7 +157,7 @@ def get_inputs_from_cli(args):
parser.add_argument(
"-p",
"--platform",
- type=parse_choices({"all", "docker", "k8s", "cloudformation", "openapi", "terraform", "serverless", "bicep"}),
+ type=parse_choices({"all", "docker", "k8s", "cloudformation", "openapi", "terraform", "serverless", "bicep", "n8n"}),
required=False,
default="all",
help="Platform to scan, applies only to the engine_iac tool and it is possible to select several {all, docker, k8s, cloudformation, openapi, terraform}",
@@ -262,7 +263,7 @@ def get_inputs_from_cli(args):
)
TOOLS = {
- "engine_iac": ["checkov", "kics", "kubescape"],
+ "engine_iac": ["checkov", "kics", "kubescape", "conftest"],
"engine_secret": ["trufflehog", "gitleaks", "all_tools"],
"engine_container": ["prisma", "trivy"],
"engine_dependencies": ["xray", "dependency_check", "trivy"],
diff --git a/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py b/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py
index fa4f1ac86..01f852163 100755
--- a/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py
+++ b/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py
@@ -58,6 +58,7 @@ class DefectDojoPlatform(VulnerabilityManagementGateway):
scan_type_mapping = {
"CHECKOV": "Checkov Scan",
+ "CONFTEST": "Conftest Scan",
"PRISMA": "Twistlock Image Scan",
"XRAY": "JFrog Xray On Demand Binary Scan",
"TRUFFLEHOG": "Trufflehog Scan",
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/applications/runner_iac_scan.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/applications/runner_iac_scan.py
index 26ce66e4d..a7291a742 100644
--- a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/applications/runner_iac_scan.py
+++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/applications/runner_iac_scan.py
@@ -10,6 +10,9 @@
from devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.kics.kics_tool import (
KicsTool
)
+from devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool import (
+ ConftestTool
+)
def runner_engine_iac(dict_args, tool, secret_tool, devops_platform_gateway, remote_config_source_gateway, env):
@@ -20,7 +23,8 @@ def runner_engine_iac(dict_args, tool, secret_tool, devops_platform_gateway, rem
tools = {
"CHECKOV": CheckovTool(),
"KUBESCAPE": KubescapeTool(),
- "KICS": KicsTool()
+ "KICS": KicsTool(),
+ "CONFTEST": ConftestTool(),
}
if tool in tools:
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/__init__.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/conftest_deserealizator.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/conftest_deserealizator.py
new file mode 100644
index 000000000..7ad5ea7cf
--- /dev/null
+++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/conftest_deserealizator.py
@@ -0,0 +1,54 @@
+from devsecops_engine_tools.engine_core.src.domain.model.finding import (
+ Category,
+ Finding,
+)
+from datetime import datetime
+from dataclasses import dataclass
+
+
+@dataclass
+class ConftestDeserealizator:
+ @classmethod
+ def get_list_finding(
+ cls, results_scan_list: list, default_severity: str, default_category: str, rules_config: dict = None
+ ) -> "list[Finding]":
+ rules_lookup = {}
+ for rule_group in (rules_config or {}).values():
+ for rule_id, rule_data in rule_group.items():
+ rules_lookup[rule_id] = rule_data
+
+ list_open_findings = []
+
+ for file_result in results_scan_list:
+ filename = file_result.get("filename", "unknown")
+
+ for failure in file_result.get("failures", []) or []:
+ msg = failure.get("msg", "unknown")
+ metadata = failure.get("metadata", {}) or {}
+ query = metadata.get("query", "unknown")
+ node_type = metadata.get("node_type", "unknown")
+ node_id = metadata.get("node_id", "unknown")
+ where = f"{filename}: {node_type}.{node_id}"
+
+ rule_id = metadata.get("id", "")
+ rule_meta = rules_lookup.get(rule_id, {})
+
+ severity = rule_meta.get("severity", default_severity).lower()
+ category_str = rule_meta.get("category", default_category).lower()
+
+ finding_open = Finding(
+ id=rule_id or query,
+ cvss=None,
+ where=where,
+ description=msg,
+ severity=severity,
+ identification_date=datetime.now().strftime("%d%m%Y"),
+ published_date_cve=None,
+ module="engine_iac",
+ category=Category(category_str),
+ requirements=rule_meta.get("url"),
+ tool="Conftest",
+ )
+ list_open_findings.append(finding_open)
+
+ return list_open_findings
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/conftest_tool.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/conftest_tool.py
new file mode 100644
index 000000000..17043842e
--- /dev/null
+++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/conftest/conftest_tool.py
@@ -0,0 +1,219 @@
+import subprocess
+import json
+import platform
+import requests
+import os
+import shutil
+from typing import List
+
+from devsecops_engine_tools.engine_sast.engine_iac.src.domain.model.context_iac import (
+ ContextIac,
+)
+from devsecops_engine_tools.engine_sast.engine_iac.src.domain.model.gateways.tool_gateway import (
+ ToolGateway,
+)
+from devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_deserealizator import (
+ ConftestDeserealizator,
+)
+from devsecops_engine_tools.engine_utilities.utils.utils import Utils
+from devsecops_engine_tools.engine_utilities.utils.logger_info import MyLogger
+from devsecops_engine_tools.engine_utilities import settings
+
+logger = MyLogger.__call__(**settings.SETTING_LOGGER).get_logger()
+
+PLATFORM_BINARY_MAP = {
+ "Linux": ("conftest_{version}_Linux_x86_64.tar.gz", "conftest"),
+ "Darwin": ("conftest_{version}_Darwin_x86_64.tar.gz", "conftest"),
+ "Windows": ("conftest_{version}_Windows_x86_64.zip", "conftest.exe"),
+}
+
+
+class ConftestTool(ToolGateway):
+ TOOL_CONFTEST = "CONFTEST"
+ RESULTS_FILE = "results_conftest.json"
+
+ def run_tool(self, config_tool, folders_to_scan, **kwargs):
+ conftest_config = config_tool.get(self.TOOL_CONFTEST, {})
+ version = conftest_config.get("VERSION")
+ default_severity = conftest_config.get("DEFAULT_SEVERITY", "low")
+ default_category = conftest_config.get("DEFAULT_CATEGORY", "vulnerability")
+
+ secret_tool = kwargs.get("secret_tool")
+ secret_external_checks = kwargs.get("secret_external_checks")
+ work_folder = kwargs.get("work_folder")
+ platform_to_scan = kwargs.get("platform_to_scan") or None
+
+ policy_path = self._resolve_policy_path(
+ conftest_config, secret_tool, secret_external_checks, work_folder
+ )
+
+ os_platform = platform.system()
+ command_prefix = self._install_binary(version, os_platform)
+
+ if command_prefix is None:
+ return [], None
+
+ results = self._execute_conftest(folders_to_scan, command_prefix, policy_path, namespaces=platform_to_scan)
+
+ if results is None:
+ return [], None
+
+ results_path = os.path.abspath(self.RESULTS_FILE)
+ with open(results_path, "w") as f:
+ json.dump(results, f, indent=4)
+
+ deserealizator = ConftestDeserealizator()
+ findings_list = deserealizator.get_list_finding(
+ results, default_severity, default_category,
+ rules_config=conftest_config.get("RULES", {})
+ )
+
+ return findings_list, results_path
+
+ def _resolve_policy_path(
+ self, conftest_config, secret_tool, secret_external_checks, work_folder
+ ):
+ if conftest_config.get("USE_EXTERNAL_CHECKS_DIR", False):
+ util = Utils()
+ util.configurate_external_checks(
+ self.TOOL_CONFTEST,
+ {self.TOOL_CONFTEST: conftest_config},
+ secret_tool,
+ secret_external_checks,
+ agent_work_folder=work_folder,
+ )
+ base_policies_path = os.path.join(work_folder, "rules", "conftest")
+ policies_subfolder = conftest_config.get(
+ "EXTERNAL_DIR_POLICIES_PATH", ""
+ ).strip("/\\")
+
+ if policies_subfolder:
+ candidate_path = os.path.join(base_policies_path, policies_subfolder)
+ if os.path.isdir(candidate_path):
+ return candidate_path
+
+ return base_policies_path
+
+ return conftest_config.get("POLICY_PATH", "policy")
+
+ def get_iac_context_from_results(self, path_file_results: str) -> List[ContextIac]:
+ with open(path_file_results, "r") as f:
+ results = json.load(f)
+
+ context_list = []
+ for file_result in results:
+ filename = file_result.get("filename", "unknown")
+ for failure in file_result.get("failures", []) or []:
+ msg = failure.get("msg", "unknown")
+ metadata = failure.get("metadata", {}) or {}
+ finding_id = metadata.get("id") or metadata.get("query", "unknown")
+ severity = metadata.get("severity", "medium").lower()
+
+ context_iac = ContextIac(
+ id=finding_id,
+ check_name=msg,
+ check_class="conftest",
+ severity=severity,
+ where=filename,
+ resource=filename,
+ description=msg,
+ module="engine_iac",
+ tool="Conftest",
+ )
+ context_list.append(context_iac)
+
+ return context_list
+
+ def _install_binary(self, version: str, os_platform: str) -> str:
+ if os_platform not in PLATFORM_BINARY_MAP:
+ logger.warning(f"Unsupported platform for conftest: {os_platform}")
+ return None
+
+ filename_template, binary_name = PLATFORM_BINARY_MAP[os_platform]
+ filename = filename_template.format(version=version)
+
+ if os.path.isfile(binary_name):
+ logger.info(f"Conftest binary already present: {binary_name}")
+ return f"./{binary_name}"
+
+ binary_in_path = shutil.which(binary_name) or shutil.which("conftest")
+ if binary_in_path:
+ logger.info(f"Conftest binary found in PATH: {binary_in_path}")
+ return binary_in_path
+
+ base_url = (
+ f"https://github.com/open-policy-agent/conftest/releases/download/v{version}/"
+ )
+ url = base_url + filename
+
+ try:
+ logger.info(f"Downloading conftest from {url}")
+ response = requests.get(url, allow_redirects=True, timeout=60)
+ response.raise_for_status()
+
+ with open(filename, "wb") as f:
+ f.write(response.content)
+
+ if filename.endswith(".tar.gz"):
+ Utils().extract_targz_file(filename, ".")
+ elif filename.endswith(".zip"):
+ Utils().unzip_file(filename, ".")
+
+ if os_platform != "Windows":
+ subprocess.run(["chmod", "+x", binary_name], check=True)
+
+ return f"./{binary_name}"
+
+ except Exception as e:
+ logger.error(f"Error installing conftest: {e}")
+ return None
+
+ def _execute_conftest(
+ self, folders_to_scan: list, command_prefix: str, policy_path: str, namespaces=None
+ ):
+ files_to_test = self._collect_files(folders_to_scan)
+
+ if not files_to_test:
+ logger.warning("No files found to scan with conftest")
+ return []
+
+ if namespaces:
+ namespace_args = []
+ for ns in namespaces:
+ namespace_args += ["--namespace", ns]
+ else:
+ namespace_args = ["--all-namespaces"]
+
+ command = (
+ [command_prefix, "test"]
+ + files_to_test
+ + ["--policy", policy_path]
+ + namespace_args
+ + ["--output", "json", "--no-fail"]
+ )
+
+ try:
+ result = subprocess.run(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ output = result.stdout.decode("utf-8").strip()
+ if not output:
+ logger.warning("Conftest returned empty output")
+ return None
+ return json.loads(output)
+ except Exception as e:
+ logger.error(f"Error executing conftest: {e}")
+ return None
+
+ def _collect_files(self, folders_to_scan: list) -> list:
+ files = []
+ for path in folders_to_scan:
+ if os.path.isfile(path):
+ files.append(path)
+ elif os.path.isdir(path):
+ for root, _, filenames in os.walk(path):
+ for fname in filenames:
+ files.append(os.path.join(root, fname))
+ return files
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/__init__.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/test_conftest_deserealizator.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/test_conftest_deserealizator.py
new file mode 100644
index 000000000..025b26f3b
--- /dev/null
+++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/test_conftest_deserealizator.py
@@ -0,0 +1,145 @@
+import unittest
+from datetime import datetime
+from devsecops_engine_tools.engine_core.src.domain.model.finding import Category, Finding
+from devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_deserealizator import (
+ ConftestDeserealizator,
+)
+
+
+class TestConftestDeserealizator(unittest.TestCase):
+ def setUp(self):
+ self.deserealizator = ConftestDeserealizator()
+
+ def test_get_list_finding_empty(self):
+ result = self.deserealizator.get_list_finding([], "high", "vulnerability")
+ self.assertEqual(result, [])
+
+ def test_get_list_finding_no_failures(self):
+ results = [{"filename": "deploy.yaml", "failures": [], "warnings": []}]
+ result = self.deserealizator.get_list_finding(results, "high", "vulnerability")
+ self.assertEqual(result, [])
+
+ def test_get_list_finding_single_failure(self):
+ results = [
+ {
+ "filename": "deploy.yaml",
+ "failures": [
+ {
+ "msg": "Containers must not run as root",
+ "metadata": {"query": "data.main.deny"},
+ }
+ ],
+ }
+ ]
+ findings = self.deserealizator.get_list_finding(results, "high", "vulnerability")
+
+ self.assertEqual(len(findings), 1)
+ finding = findings[0]
+ self.assertEqual(finding.id, "data.main.deny") # no metadata.id → fallback to query
+ self.assertEqual(finding.description, "Containers must not run as root")
+ self.assertEqual(finding.where, "deploy.yaml: unknown.unknown")
+ self.assertEqual(finding.severity, "high")
+ self.assertEqual(finding.category, Category.VULNERABILITY)
+ self.assertEqual(finding.tool, "Conftest")
+ self.assertEqual(finding.module, "engine_iac")
+
+ def test_get_list_finding_where_uses_node_type_and_node_id(self):
+ results = [
+ {
+ "filename": "workflow.json",
+ "failures": [
+ {
+ "msg": "Insecure node",
+ "metadata": {
+ "query": "data.n8n.deny",
+ "id": "CONF_N8N_BC_3",
+ "node_type": "n8n-nodes-base.executeCommand",
+ "node_id": "8a77aa44-0f57-4f6b-985a-4b5989138621",
+ },
+ }
+ ],
+ }
+ ]
+
+ findings = self.deserealizator.get_list_finding(results, "high", "vulnerability")
+ self.assertEqual(findings[0].where, "workflow.json: n8n-nodes-base.executeCommand.8a77aa44-0f57-4f6b-985a-4b5989138621")
+
+ def test_get_list_finding_uses_rules_config_severity(self):
+ results = [
+ {
+ "filename": "main.tf",
+ "failures": [
+ {
+ "msg": "S3 bucket not encrypted",
+ "metadata": {"query": "data.n8n.deny", "id": "CONF_N8N_BC_1"},
+ }
+ ],
+ }
+ ]
+ rules_config = {
+ "RULES_N8N": {
+ "CONF_N8N_BC_1": {"severity": "Critical", "category": "Vulnerability"}
+ }
+ }
+ findings = self.deserealizator.get_list_finding(
+ results, "low", "compliance", rules_config=rules_config
+ )
+
+ self.assertEqual(len(findings), 1)
+ self.assertEqual(findings[0].id, "CONF_N8N_BC_1")
+ self.assertEqual(findings[0].severity, "critical")
+ self.assertEqual(findings[0].category, Category.VULNERABILITY)
+
+ def test_get_list_finding_multiple_files(self):
+ results = [
+ {
+ "filename": "file1.yaml",
+ "failures": [
+ {"msg": "Error 1", "metadata": {"query": "data.main.deny"}}
+ ],
+ },
+ {
+ "filename": "file2.tf",
+ "failures": [
+ {"msg": "Error 2", "metadata": {"query": "data.tf.deny"}},
+ {"msg": "Error 3", "metadata": {"query": "data.tf.deny"}},
+ ],
+ },
+ ]
+ findings = self.deserealizator.get_list_finding(results, "medium", "vulnerability")
+ self.assertEqual(len(findings), 3)
+
+ def test_get_list_finding_failures_none_handled(self):
+ results = [{"filename": "deploy.yaml", "failures": None}]
+ findings = self.deserealizator.get_list_finding(results, "high", "vulnerability")
+ self.assertEqual(findings, [])
+
+ def test_get_list_finding_rules_config_url_becomes_requirements(self):
+ results = [
+ {
+ "filename": "deploy.yaml",
+ "failures": [
+ {
+ "msg": "Missing label",
+ "metadata": {"query": "data.n8n.deny", "id": "CONF_N8N_BC_2"},
+ }
+ ],
+ }
+ ]
+ rules_config = {
+ "RULES_N8N": {
+ "CONF_N8N_BC_2": {
+ "severity": "medium",
+ "category": "vulnerability",
+ "url": "https://example.com/policy",
+ }
+ }
+ }
+ findings = self.deserealizator.get_list_finding(
+ results, "high", "vulnerability", rules_config=rules_config
+ )
+ self.assertEqual(findings[0].requirements, "https://example.com/policy")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/test_conftest_tool.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/test_conftest_tool.py
new file mode 100644
index 000000000..49e8f085b
--- /dev/null
+++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/conftest/test_conftest_tool.py
@@ -0,0 +1,370 @@
+import json
+import os
+import platform
+import unittest
+from unittest.mock import MagicMock, mock_open, patch
+
+from devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool import (
+ ConftestTool,
+)
+
+
+CONFIG_TOOL = {
+ "CONFTEST": {
+ "VERSION": "0.56.0",
+ "POLICY_PATH": "policy",
+ "USE_EXTERNAL_CHECKS_DIR": False,
+ "EXTERNAL_DIR_OWNER": "",
+ "EXTERNAL_DIR_REPOSITORY": "",
+ "EXTERNAL_DIR_POLICIES_PATH": "policy",
+ "APP_ID_GITHUB": "",
+ "INSTALLATION_ID_GITHUB": "",
+ "DEFAULT_SEVERITY": "high",
+ "DEFAULT_CATEGORY": "vulnerability",
+ }
+}
+
+CONFIG_TOOL_EXTERNAL = {
+ "CONFTEST": {
+ "VERSION": "0.56.0",
+ "USE_EXTERNAL_CHECKS_DIR": True,
+ "EXTERNAL_DIR_OWNER": "my-org",
+ "EXTERNAL_DIR_REPOSITORY": "conftest-policies",
+ "EXTERNAL_DIR_POLICIES_PATH": "policy",
+ "APP_ID_GITHUB": "123",
+ "INSTALLATION_ID_GITHUB": "456",
+ "DEFAULT_SEVERITY": "high",
+ "DEFAULT_CATEGORY": "vulnerability",
+ }
+}
+
+SAMPLE_RESULTS = [
+ {
+ "filename": "deploy.yaml",
+ "namespace": "main",
+ "successes": 1,
+ "failures": [
+ {
+ "msg": "Containers must not run as root",
+ "metadata": {"query": "data.main.deny", "id": "CONF_N8N_BC_3"},
+ }
+ ],
+ "warnings": [],
+ }
+]
+
+
+class TestConftestTool(unittest.TestCase):
+ def setUp(self):
+ self.tool = ConftestTool()
+
+ # ------------------------------------------------------------------ #
+ # _collect_files #
+ # ------------------------------------------------------------------ #
+
+ @patch("os.path.isfile", return_value=True)
+ def test_collect_files_single_file(self, _):
+ result = self.tool._collect_files(["deploy.yaml"])
+ self.assertEqual(result, ["deploy.yaml"])
+
+ @patch("os.walk", return_value=[("/folder", [], ["a.yaml", "b.tf"])])
+ @patch("os.path.isfile", return_value=False)
+ @patch("os.path.isdir", return_value=True)
+ def test_collect_files_directory(self, _isdir, _isfile, _walk):
+ result = self.tool._collect_files(["/folder"])
+ self.assertEqual(sorted(result), sorted(["/folder/a.yaml", "/folder/b.tf"]))
+
+ # ------------------------------------------------------------------ #
+ # _resolve_policy_path #
+ # ------------------------------------------------------------------ #
+
+ def test_resolve_policy_path_local(self):
+ cfg = {"USE_EXTERNAL_CHECKS_DIR": False, "POLICY_PATH": "my_policies"}
+ result = self.tool._resolve_policy_path(cfg, None, None, "/tmp")
+ self.assertEqual(result, "my_policies")
+
+ def test_resolve_policy_path_local_default(self):
+ cfg = {"USE_EXTERNAL_CHECKS_DIR": False}
+ result = self.tool._resolve_policy_path(cfg, None, None, "/tmp")
+ self.assertEqual(result, "policy")
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.Utils"
+ )
+ def test_resolve_policy_path_external_dir(self, mock_utils_cls):
+ mock_utils = MagicMock()
+ mock_utils_cls.return_value = mock_utils
+
+ cfg = {
+ "USE_EXTERNAL_CHECKS_DIR": True,
+ "EXTERNAL_DIR_OWNER": "org",
+ "EXTERNAL_DIR_REPOSITORY": "repo",
+ "EXTERNAL_DIR_POLICIES_PATH": "policy",
+ "APP_ID_GITHUB": "1",
+ "INSTALLATION_ID_GITHUB": "2",
+ }
+ result = self.tool._resolve_policy_path(cfg, None, "token:abc", "/tmp")
+
+ mock_utils.configurate_external_checks.assert_called_once_with(
+ "CONFTEST",
+ {"CONFTEST": cfg},
+ None,
+ "token:abc",
+ agent_work_folder="/tmp",
+ )
+ self.assertEqual(result, os.path.join("/tmp", "rules", "conftest"))
+
+ @patch("os.path.isdir", return_value=True)
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.Utils"
+ )
+ def test_resolve_policy_path_external_custom_subfolder(self, mock_utils_cls, _isdir):
+ mock_utils_cls.return_value = MagicMock()
+ cfg = {
+ "USE_EXTERNAL_CHECKS_DIR": True,
+ "EXTERNAL_DIR_POLICIES_PATH": "rego_rules",
+ }
+ result = self.tool._resolve_policy_path(cfg, None, None, "/workspace")
+ self.assertEqual(
+ result,
+ os.path.join("/workspace", "rules", "conftest", "rego_rules"),
+ )
+
+ # ------------------------------------------------------------------ #
+ # _install_binary #
+ # ------------------------------------------------------------------ #
+
+ @patch("os.path.isfile", return_value=True)
+ def test_install_binary_already_present_linux(self, _):
+ result = self.tool._install_binary("0.56.0", "Linux")
+ self.assertEqual(result, "./conftest")
+
+ @patch("os.path.isfile", return_value=True)
+ def test_install_binary_already_present_windows(self, _):
+ result = self.tool._install_binary("0.56.0", "Windows")
+ self.assertEqual(result, "./conftest.exe")
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.requests.get"
+ )
+ @patch("shutil.which", return_value="/usr/local/bin/conftest")
+ @patch("os.path.isfile", return_value=False)
+ def test_install_binary_uses_path_before_download(
+ self, _isfile, _which, mock_get
+ ):
+ result = self.tool._install_binary("0.56.0", "Linux")
+ self.assertEqual(result, "/usr/local/bin/conftest")
+ mock_get.assert_not_called()
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.logger"
+ )
+ def test_install_binary_unsupported_platform(self, mock_logger):
+ result = self.tool._install_binary("0.56.0", "SunOS")
+ self.assertIsNone(result)
+ mock_logger.warning.assert_called_once()
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.Utils"
+ )
+ @patch("shutil.which", return_value=None)
+ @patch("subprocess.run")
+ @patch("builtins.open", new_callable=mock_open)
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.requests.get"
+ )
+ @patch("os.path.isfile", return_value=False)
+ def test_install_binary_downloads_and_extracts_linux(
+ self, _isfile, mock_get, mock_file, mock_subprocess, _which, mock_utils_cls
+ ):
+ mock_get.return_value = MagicMock(status_code=200, content=b"binary")
+ mock_get.return_value.raise_for_status = MagicMock()
+ mock_subprocess.return_value = MagicMock(returncode=0)
+ mock_utils = MagicMock()
+ mock_utils_cls.return_value = mock_utils
+
+ result = self.tool._install_binary("0.56.0", "Linux")
+ self.assertEqual(result, "./conftest")
+ mock_utils.extract_targz_file.assert_called_once_with(
+ "conftest_0.56.0_Linux_x86_64.tar.gz", "."
+ )
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.requests.get",
+ side_effect=Exception("network error"),
+ )
+ @patch("shutil.which", return_value=None)
+ @patch("os.path.isfile", return_value=False)
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.logger"
+ )
+ def test_install_binary_download_failure(self, mock_logger, _isfile, _which, _get):
+ result = self.tool._install_binary("0.56.0", "Linux")
+ self.assertIsNone(result)
+ mock_logger.error.assert_called_once()
+
+ # ------------------------------------------------------------------ #
+ # _execute_conftest #
+ # ------------------------------------------------------------------ #
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.logger"
+ )
+ def test_execute_conftest_no_files(self, mock_logger):
+ with patch.object(self.tool, "_collect_files", return_value=[]):
+ result = self.tool._execute_conftest([], "./conftest", "policy")
+ self.assertEqual(result, [])
+ mock_logger.warning.assert_called_once()
+
+ @patch("subprocess.run")
+ def test_execute_conftest_success(self, mock_subprocess):
+ mock_subprocess.return_value = MagicMock(
+ stdout=json.dumps(SAMPLE_RESULTS).encode("utf-8")
+ )
+ with patch.object(self.tool, "_collect_files", return_value=["deploy.yaml"]):
+ result = self.tool._execute_conftest(["deploy.yaml"], "./conftest", "policy")
+ self.assertEqual(result, SAMPLE_RESULTS)
+
+ @patch("subprocess.run")
+ def test_execute_conftest_single_policy_arg(self, mock_subprocess):
+ """Verifies a single --policy flag is built from the policy_path string."""
+ mock_subprocess.return_value = MagicMock(
+ stdout=json.dumps(SAMPLE_RESULTS).encode("utf-8")
+ )
+ with patch.object(self.tool, "_collect_files", return_value=["deploy.yaml"]):
+ self.tool._execute_conftest(
+ ["deploy.yaml"], "./conftest", "policy"
+ )
+ cmd = mock_subprocess.call_args[0][0]
+ self.assertEqual(cmd.count("--policy"), 1)
+ self.assertIn("policy", cmd)
+ self.assertIn("--all-namespaces", cmd)
+
+ @patch("subprocess.run")
+ def test_execute_conftest_empty_output(self, mock_subprocess):
+ mock_subprocess.return_value = MagicMock(stdout=b"")
+ with patch.object(self.tool, "_collect_files", return_value=["deploy.yaml"]):
+ result = self.tool._execute_conftest(["deploy.yaml"], "./conftest", "policy")
+ self.assertIsNone(result)
+
+ @patch("subprocess.run", side_effect=Exception("subprocess failed"))
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.logger"
+ )
+ def test_execute_conftest_exception(self, mock_logger, _):
+ with patch.object(self.tool, "_collect_files", return_value=["deploy.yaml"]):
+ result = self.tool._execute_conftest(["deploy.yaml"], "./conftest", "policy")
+ self.assertIsNone(result)
+ mock_logger.error.assert_called_once()
+
+ @patch("subprocess.run")
+ def test_execute_conftest_uses_platform_namespace(self, mock_subprocess):
+ """Verifies --namespace flag is used per platform instead of --all-namespaces."""
+ mock_subprocess.return_value = MagicMock(
+ stdout=json.dumps(SAMPLE_RESULTS).encode("utf-8")
+ )
+ with patch.object(self.tool, "_collect_files", return_value=["workflow.json"]):
+ self.tool._execute_conftest(
+ ["workflow.json"], "./conftest", "policy", namespaces=["n8n"]
+ )
+ cmd = mock_subprocess.call_args[0][0]
+ self.assertIn("--namespace", cmd)
+ self.assertIn("n8n", cmd)
+ self.assertNotIn("--all-namespaces", cmd)
+
+ # ------------------------------------------------------------------ #
+ # run_tool #
+ # ------------------------------------------------------------------ #
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.logger"
+ )
+ def test_run_tool_binary_install_fails(self, _):
+ with patch.object(self.tool, "_install_binary", return_value=None):
+ findings, path = self.tool.run_tool(CONFIG_TOOL, ["folder"])
+ self.assertEqual(findings, [])
+ self.assertIsNone(path)
+
+ @patch("builtins.open", new_callable=mock_open)
+ @patch("json.dump")
+ @patch("os.path.abspath", return_value="/tmp/results_conftest.json")
+ def test_run_tool_no_results(self, _abspath, _dump, _open):
+ with patch.object(self.tool, "_install_binary", return_value="./conftest"):
+ with patch.object(self.tool, "_execute_conftest", return_value=[]):
+ findings, path = self.tool.run_tool(CONFIG_TOOL, ["folder"])
+ self.assertEqual(findings, [])
+ self.assertEqual(path, "/tmp/results_conftest.json")
+
+ @patch("builtins.open", new_callable=mock_open)
+ @patch("json.dump")
+ @patch("os.path.abspath", return_value="/tmp/results_conftest.json")
+ def test_run_tool_returns_findings_and_path(self, _abspath, _dump, _open):
+ with patch.object(self.tool, "_install_binary", return_value="./conftest"):
+ with patch.object(
+ self.tool, "_execute_conftest", return_value=SAMPLE_RESULTS
+ ):
+ findings, path = self.tool.run_tool(CONFIG_TOOL, ["folder"])
+
+ self.assertEqual(len(findings), 1)
+ self.assertEqual(path, "/tmp/results_conftest.json")
+
+ @patch(
+ "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.conftest.conftest_tool.Utils"
+ )
+ @patch("builtins.open", new_callable=mock_open)
+ @patch("json.dump")
+ @patch("os.path.abspath", return_value="/tmp/results_conftest.json")
+ def test_run_tool_uses_external_dir_policies(
+ self, _abspath, _dump, _open, mock_utils_cls
+ ):
+ mock_utils = MagicMock()
+ mock_utils_cls.return_value = mock_utils
+
+ with patch.object(self.tool, "_install_binary", return_value="./conftest"):
+ with patch.object(
+ self.tool, "_execute_conftest", return_value=SAMPLE_RESULTS
+ ) as mock_exec:
+ findings, path = self.tool.run_tool(
+ CONFIG_TOOL_EXTERNAL,
+ ["folder"],
+ secret_tool=None,
+ secret_external_checks="github_token:abc123",
+ work_folder="/tmp",
+ )
+
+ mock_utils.configurate_external_checks.assert_called_once()
+ # _execute_conftest receives a string path and no namespaces
+ expected_policy_path = os.path.join("/tmp", "rules", "conftest")
+ mock_exec.assert_called_once_with(["folder"], "./conftest", expected_policy_path, namespaces=None)
+ self.assertEqual(len(findings), 1)
+
+ # ------------------------------------------------------------------ #
+ # get_iac_context_from_results #
+ # ------------------------------------------------------------------ #
+
+ @patch(
+ "builtins.open",
+ new_callable=mock_open,
+ read_data=json.dumps(SAMPLE_RESULTS),
+ )
+ def test_get_iac_context_from_results(self, _):
+ context_list = self.tool.get_iac_context_from_results("results_conftest.json")
+ self.assertEqual(len(context_list), 1)
+ ctx = context_list[0]
+ self.assertEqual(ctx.id, "CONF_N8N_BC_3")
+ self.assertEqual(ctx.tool, "Conftest")
+ self.assertEqual(ctx.where, "deploy.yaml")
+ self.assertEqual(ctx.check_name, "Containers must not run as root")
+
+ @patch(
+ "builtins.open",
+ new_callable=mock_open,
+ read_data=json.dumps([{"filename": "f.yaml", "failures": None}]),
+ )
+ def test_get_iac_context_from_results_null_failures(self, _):
+ context_list = self.tool.get_iac_context_from_results("results_conftest.json")
+ self.assertEqual(context_list, [])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/devsecops_engine_tools/engine_utilities/defect_dojo/domain/serializers/import_scan.py b/tools/devsecops_engine_tools/engine_utilities/defect_dojo/domain/serializers/import_scan.py
index 8440fdde7..3a24def86 100755
--- a/tools/devsecops_engine_tools/engine_utilities/defect_dojo/domain/serializers/import_scan.py
+++ b/tools/devsecops_engine_tools/engine_utilities/defect_dojo/domain/serializers/import_scan.py
@@ -36,6 +36,7 @@
"Checkmarx Scan",
"Checkmarx Scan detailed",
"Checkov Scan",
+ "Conftest Scan",
"Clair Klar Scan",
"Clair Scan",
"Cloudsploit Scan",
diff --git a/tools/devsecops_engine_tools/engine_utilities/utils/utils.py b/tools/devsecops_engine_tools/engine_utilities/utils/utils.py
index b0fe60d55..63a9a460c 100755
--- a/tools/devsecops_engine_tools/engine_utilities/utils/utils.py
+++ b/tools/devsecops_engine_tools/engine_utilities/utils/utils.py
@@ -86,7 +86,7 @@ def configurate_external_checks(self, tool, config_tool, secret_tool, secret_ext
logger.warning("The secret is not configured for external controls")
- elif config_tool[tool]["USE_EXTERNAL_CHECKS_GIT"] and platform.system() in (
+ elif config_tool[tool].get("USE_EXTERNAL_CHECKS_GIT", False) and platform.system() in (
"Linux", "Darwin",
):
config_knowns_hosts(
@@ -99,7 +99,7 @@ def configurate_external_checks(self, tool, config_tool, secret_tool, secret_ext
ssh_key_password = decode_base64(secret["repository_ssh_password"])
agent_env = add_ssh_private_key(ssh_key_file_path, ssh_key_password)
- elif config_tool[tool]["USE_EXTERNAL_CHECKS_DIR"]:
+ elif config_tool[tool].get("USE_EXTERNAL_CHECKS_DIR", False):
if not github_token:
github_token = github_api.get_installation_access_token(
secret.get("github_apps"),