Skip to content

Commit a3267d3

Browse files
committed
added security toolkits
1 parent 8136a8a commit a3267d3

File tree

11 files changed

+1288
-4
lines changed

11 files changed

+1288
-4
lines changed

parrot/tools/security/prowler/executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def _build_aws_args(self, **kwargs) -> list[str]:
8888
# Region filtering (updated for Prowler v4+)
8989
regions = kwargs.get("filter_regions", self.config.filter_regions)
9090
if regions:
91-
args.extend(["--region", ",".join(regions)])
91+
args.extend(["-f", ",".join(regions)])
9292

9393
# AWS profile
9494
profile = kwargs.get("aws_profile", self.config.aws_profile)

parrot/tools/security/reports/generator.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
from datetime import datetime
1010
from pathlib import Path
1111
from typing import Optional
12-
1312
from jinja2 import Environment, FileSystemLoader
14-
1513
from ..models import (
1614
ComplianceFramework,
1715
ConsolidatedReport,
@@ -36,7 +34,7 @@ class ReportGenerator:
3634
)
3735
"""
3836

39-
def __init__(self, output_dir: str = "/tmp/security-reports"):
37+
def __init__(self, output_dir: str = "/tmp/reports"):
4038
"""Initialize the ReportGenerator.
4139
4240
Args:

parrot/tools/security/scoutsuite/__init__.py

Whitespace-only changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""ScoutSuite-specific configuration.
2+
3+
ScoutSuite is an open source multi-cloud security-auditing tool.
4+
"""
5+
6+
from typing import Optional
7+
8+
from pydantic import Field
9+
10+
from ..base_executor import BaseExecutorConfig
11+
12+
13+
class ScoutSuiteConfig(BaseExecutorConfig):
14+
"""Configuration for ScoutSuite security scanner.
15+
16+
Extends BaseExecutorConfig with ScoutSuite-specific options.
17+
"""
18+
19+
# Execution overrides (ScoutSuite usually runs directly via CLI)
20+
use_docker: bool = Field(
21+
default=False,
22+
description="Run via Docker or direct CLI (usually False for ScoutSuite)",
23+
)
24+
25+
# Provider selection
26+
provider: str = Field(
27+
default="aws",
28+
description="Cloud provider: aws, azure, gcp, aliyun, oracle",
29+
)
30+
31+
# Output configuration
32+
result_format: str = Field(
33+
default="json",
34+
description="Output result format, e.g json",
35+
)
36+
report_name: Optional[str] = Field(
37+
default="scoutsuite-report",
38+
description="Name of the generated report file without extension",
39+
)
40+
report_dir: Optional[str] = Field(
41+
default=None,
42+
description="Custom directory for the report (maps to output_directory or results_dir if none)",
43+
)
44+
45+
# Filtering Options
46+
regions: list[str] = Field(
47+
default_factory=list,
48+
description="Specific regions to scan",
49+
)
50+
services: list[str] = Field(
51+
default_factory=list,
52+
description="Specific services to scan",
53+
)
54+
55+
model_config = {"extra": "ignore"}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""ScoutSuite executor for running cloud security scans.
2+
3+
Extends BaseExecutor to provide ScoutSuite-specific CLI argument building
4+
and scan execution methods.
5+
"""
6+
7+
from typing import Optional
8+
import tempfile
9+
from pathlib import Path
10+
11+
from ..base_executor import BaseExecutor
12+
from .config import ScoutSuiteConfig
13+
14+
15+
class ScoutSuiteExecutor(BaseExecutor):
16+
"""Executes ScoutSuite security scans.
17+
18+
ScoutSuite CLI pattern: `scout <provider> [options]`
19+
20+
Example:
21+
scout aws --report-dir ./aws-scan-2023-12-18 \\
22+
--report-name aws-report \\
23+
--result-format json \\
24+
--access-key-id ACCESS_KEY_ID \\
25+
--secret-access-key SECRET_KEY
26+
"""
27+
28+
def __init__(self, config: Optional[ScoutSuiteConfig] = None):
29+
"""Initialize the ScoutSuite executor.
30+
31+
Args:
32+
config: ScoutSuite configuration. Uses defaults if not provided.
33+
"""
34+
super().__init__(config or ScoutSuiteConfig())
35+
self.config: ScoutSuiteConfig = self.config # type narrowing
36+
self.expected_exit_codes = [0, 1] # Scout returned 1 on findings sometimes
37+
38+
def _default_cli_name(self) -> str:
39+
"""Return the default ScoutSuite CLI binary name."""
40+
return "scout"
41+
42+
def _build_cli_args(self, **kwargs) -> list[str]:
43+
"""Build ScoutSuite CLI arguments from configuration.
44+
45+
Args:
46+
**kwargs: Override config values for this invocation.
47+
48+
Returns:
49+
List of CLI argument strings.
50+
"""
51+
provider = kwargs.get("provider", self.config.provider)
52+
args = [provider]
53+
54+
# Region filtering
55+
regions = kwargs.get("regions", self.config.regions)
56+
if regions:
57+
args.extend(["--regions", ",".join(regions)])
58+
59+
# Services to scan
60+
services = kwargs.get("services", self.config.services)
61+
if services:
62+
args.extend(["--services", ",".join(services)])
63+
64+
# Output options
65+
report_dir = kwargs.get("report_dir", self.config.report_dir or self.config.results_dir)
66+
if report_dir:
67+
args.extend(["--report-dir", str(report_dir)])
68+
69+
report_name = kwargs.get("report_name", self.config.report_name)
70+
if report_name:
71+
args.extend(["--report-name", str(report_name)])
72+
73+
result_format = kwargs.get("result_format", self.config.result_format)
74+
if result_format:
75+
args.extend(["--result-format", result_format])
76+
77+
# Passing credentials explicitly if needed by the AWS provider, though
78+
# BaseExecutor already injects these effectively as environment vars.
79+
# However, the user explicitly requested `--access-key-id` and `--secret-access-key`.
80+
if provider == "aws":
81+
aws_access_key_id = self.config.aws_access_key_id
82+
if aws_access_key_id:
83+
args.extend(["--access-key-id", aws_access_key_id])
84+
85+
aws_secret_access_key = self.config.aws_secret_access_key
86+
if aws_secret_access_key:
87+
args.extend(["--secret-access-key", aws_secret_access_key])
88+
89+
aws_session_token = self.config.aws_session_token
90+
if aws_session_token:
91+
args.extend(["--session-token", aws_session_token])
92+
93+
aws_profile = self.config.aws_profile
94+
if aws_profile:
95+
args.extend(["--profile", aws_profile])
96+
97+
return args
98+
99+
def _build_scan_kwargs(
100+
self,
101+
provider: Optional[str] = None,
102+
services: Optional[list[str]] = None,
103+
regions: Optional[list[str]] = None,
104+
report_name: Optional[str] = None,
105+
report_dir: Optional[str] = None,
106+
) -> dict:
107+
"""Build kwargs dict from scan parameters."""
108+
kwargs: dict = {}
109+
if provider:
110+
kwargs["provider"] = provider
111+
if services:
112+
kwargs["services"] = services
113+
if regions:
114+
kwargs["regions"] = regions
115+
if report_name:
116+
kwargs["report_name"] = report_name
117+
if report_dir:
118+
kwargs["report_dir"] = report_dir
119+
return kwargs
120+
121+
async def run_scan(
122+
self,
123+
provider: Optional[str] = None,
124+
services: Optional[list[str]] = None,
125+
regions: Optional[list[str]] = None,
126+
report_name: Optional[str] = None,
127+
report_dir: Optional[str] = None,
128+
) -> tuple[str, str, int]:
129+
"""Run a ScoutSuite security scan.
130+
131+
Returns:
132+
Tuple of (stdout, stderr, exit_code).
133+
"""
134+
kwargs = self._build_scan_kwargs(
135+
provider, services, regions, report_name, report_dir
136+
)
137+
return await self._execute_with_json_capture(self.execute, **kwargs)
138+
139+
async def run_scan_streaming(
140+
self,
141+
progress_callback=None,
142+
provider: Optional[str] = None,
143+
services: Optional[list[str]] = None,
144+
regions: Optional[list[str]] = None,
145+
report_name: Optional[str] = None,
146+
report_dir: Optional[str] = None,
147+
) -> tuple[str, str, int]:
148+
"""Run a ScoutSuite scan with real-time stderr streaming."""
149+
kwargs = self._build_scan_kwargs(
150+
provider, services, regions, report_name, report_dir
151+
)
152+
return await self._execute_with_json_capture(
153+
self.execute_streaming, progress_callback=progress_callback, **kwargs
154+
)
155+
156+
async def _execute_with_json_capture(self, execute_func, *args, **kwargs) -> tuple[str, str, int]:
157+
"""Run execution and capture JSON output.
158+
159+
Uses a temporary directory to manage reports if the user doesn't pass one explicitly.
160+
"""
161+
# If user provided a report directory, use it directly
162+
explicit_report_dir = kwargs.get("report_dir", self.config.report_dir or self.config.results_dir)
163+
164+
with tempfile.TemporaryDirectory() as temp_dir:
165+
temp_path = Path(temp_dir)
166+
active_report_dir = explicit_report_dir or str(temp_path)
167+
kwargs["report_dir"] = active_report_dir
168+
169+
# Make sure we use JSON format to parse results
170+
if "result_format" not in kwargs:
171+
kwargs["result_format"] = "json"
172+
173+
# Run the scan
174+
stdout, stderr, exit_code = await execute_func(*args, **kwargs)
175+
176+
# Find the generated JSON result in the scoutsuite-report directory
177+
report_name = kwargs.get("report_name", self.config.report_name)
178+
report_filename = f"{report_name}.js" if kwargs.get("result_format") == "json" else f"{report_name}.json"
179+
180+
# ScoutSuite typically generates JSON inside an HTML wrapper called scoutsuite_results_...js
181+
# Let's search broadly for .js or .json in the output dir
182+
target_dir = Path(active_report_dir)
183+
184+
# Scout creates scoutsuite-report/scoutsuite-results by default if report_name is not fully mapping
185+
results_files = list(target_dir.rglob("scoutsuite_results*.js"))
186+
if not results_files:
187+
results_files = list(target_dir.rglob("*.json"))
188+
189+
if results_files:
190+
# Read the latest file found
191+
json_content = results_files[-1].read_text(encoding="utf-8")
192+
# ScoutSuite .js files start with `scoutsuite_results = { ... }` which needs to be cleaned for pure JSON
193+
if json_content.startswith("scoutsuite_results ="):
194+
json_content = json_content.replace("scoutsuite_results =", "", 1).strip().strip(";")
195+
return json_content, stderr, exit_code
196+
else:
197+
self.logger.warning("No JSON result file found in ScoutSuite output")
198+
return stdout, stderr, exit_code
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"check_type": "terraform",
3+
"results": {
4+
"passed_checks": [
5+
{
6+
"check_id": "CKV_AWS_18",
7+
"check_name": "Ensure the S3 bucket has access logging enabled",
8+
"check_result": {"result": "PASSED"},
9+
"resource": "aws_s3_bucket.logs",
10+
"file_path": "/terraform/main.tf",
11+
"file_line_range": [1, 15],
12+
"guideline": "https://docs.bridgecrew.io/docs/s3_13-enable-logging"
13+
}
14+
],
15+
"failed_checks": [
16+
{
17+
"check_id": "CKV_AWS_21",
18+
"check_name": "Ensure the S3 bucket has versioning enabled",
19+
"check_result": {"result": "FAILED"},
20+
"resource": "aws_s3_bucket.data",
21+
"file_path": "/terraform/main.tf",
22+
"file_line_range": [20, 35],
23+
"evaluations": {"default": {"reason": "versioning not enabled"}},
24+
"guideline": "https://docs.bridgecrew.io/docs/s3_16-enable-versioning"
25+
},
26+
{
27+
"check_id": "CKV_AWS_19",
28+
"check_name": "Ensure the S3 bucket has encryption enabled",
29+
"check_result": {"result": "FAILED"},
30+
"resource": "aws_s3_bucket.data",
31+
"file_path": "/terraform/main.tf",
32+
"file_line_range": [20, 35],
33+
"guideline": "https://docs.bridgecrew.io/docs/s3_14-enable-encryption"
34+
},
35+
{
36+
"check_id": "CKV_AWS_40",
37+
"check_name": "Ensure IAM password policy requires minimum length of 14",
38+
"check_result": {"result": "FAILED"},
39+
"resource": "aws_iam_account_password_policy.strict",
40+
"file_path": "/terraform/iam.tf",
41+
"file_line_range": [1, 12],
42+
"guideline": "https://docs.bridgecrew.io/docs/iam_7-password-length"
43+
}
44+
],
45+
"skipped_checks": [
46+
{
47+
"check_id": "CKV_AWS_1",
48+
"check_name": "Ensure CloudTrail is enabled",
49+
"check_result": {"result": "SKIPPED", "suppress_comment": "Not applicable"},
50+
"resource": "aws_cloudtrail.main",
51+
"file_path": "/terraform/cloudtrail.tf",
52+
"file_line_range": [1, 5]
53+
}
54+
]
55+
},
56+
"summary": {
57+
"passed": 1,
58+
"failed": 3,
59+
"skipped": 1,
60+
"parsing_errors": 0
61+
}
62+
}

0 commit comments

Comments
 (0)