diff --git a/.gitignore b/.gitignore
index a4686169..2bfbce85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ _notes
results.*
default.json
default-iam-report.csv
-private/default-iam-results.json
+private/default-iam-results.json0
default-results-summary.csv
iam-new-principal-policy-mapping-example.json
iam-findings-example.json
diff --git a/cloudsplaining/command/create_exclusions_file.py b/cloudsplaining/command/create_exclusions_file.py
index fb7f5a20..217814ed 100644
--- a/cloudsplaining/command/create_exclusions_file.py
+++ b/cloudsplaining/command/create_exclusions_file.py
@@ -8,7 +8,6 @@
# For full license text, see the LICENSE file in the repo root
# or https://opensource.org/licenses/BSD-3-Clause
import os
-from pathlib import Path
import logging
import click
from cloudsplaining.shared.constants import EXCLUSIONS_TEMPLATE
@@ -22,14 +21,7 @@
context_settings=dict(max_content_width=160),
short_help="Creates a YML file to be used as a custom exclusions template",
)
-@click.option(
- "--output-file",
- "-o",
- type=click.Path(exists=False),
- default=os.path.join(os.getcwd(), "exclusions.yml"),
- required=True,
- help="Relative path to output file where we want to store the exclusions template.",
-)
+@click.option("-o", "--output-file", type=click.Path(exists=False), default=os.path.join(os.getcwd(), "exclusions.yml"), required=True, help="Relative path to output file where we want to store the exclusions template.")
@click.option("--verbose", "-v", "verbosity", count=True)
def create_exclusions_file(output_file: str, verbosity: int) -> None:
"""
@@ -38,11 +30,10 @@ def create_exclusions_file(output_file: str, verbosity: int) -> None:
"""
set_log_level(verbosity)
- filename = Path(output_file).resolve()
- with open(filename, "a") as file_obj:
+ with open(output_file, "a") as file_obj:
for line in EXCLUSIONS_TEMPLATE:
file_obj.write(line)
- utils.print_green(f"Success! Exclusions template file written to: {filename}")
+ utils.print_green(f"Success! Exclusions template file written to: {output_file}")
print(
"Make sure you download your account authorization details before running the scan."
"Set your AWS access keys as environment variables then run: "
diff --git a/cloudsplaining/command/create_multi_account_config_file.py b/cloudsplaining/command/create_multi_account_config_file.py
index 668759df..f78b0ac3 100644
--- a/cloudsplaining/command/create_multi_account_config_file.py
+++ b/cloudsplaining/command/create_multi_account_config_file.py
@@ -8,7 +8,6 @@
# For full license text, see the LICENSE file in the repo root
# or https://opensource.org/licenses/BSD-3-Clause
import os
-from pathlib import Path
import logging
import click
from cloudsplaining.shared.constants import MULTI_ACCOUNT_CONFIG_TEMPLATE
@@ -24,41 +23,29 @@
context_settings=dict(max_content_width=160),
short_help="Creates a YML file to be used for multi-account scanning",
)
-@click.option(
- "--output-file",
- "-o",
- "output_file",
- type=click.Path(exists=False),
- default=os.path.join(os.getcwd(), "multi-account-config.yml"),
- required=True,
- help="Relative path to output file where we want to store the multi account config template.",
-)
-@click.option(
- "-v", "--verbose", "verbosity", count=True,
-)
+@click.option("-o", "--output-file", "output_file", type=click.Path(exists=False), default=os.path.join(os.getcwd(), "multi-account-config.yml"), required=True, help="Relative path to output file where we want to store the multi account config template.")
+@click.option("-v", "--verbose", "verbosity", help="Log verbosity level.", count=True)
def create_multi_account_config_file(output_file: str, verbosity: int) -> None:
"""
Creates a YML file to be used as a multi-account config template, so users can scan many different accounts.
"""
set_log_level(verbosity)
- filename = Path(output_file).resolve()
-
- if filename.exists():
+ if os.path.exists(output_file):
logger.debug(
- "%s exists. Removing the file and replacing its contents.", filename
+ "%s exists. Removing the file and replacing its contents.", output_file
)
- filename.unlink()
+ os.remove(output_file)
- with open(filename, "a") as file_obj:
+ with open(output_file, "a") as file_obj:
for line in MULTI_ACCOUNT_CONFIG_TEMPLATE:
file_obj.write(line)
utils.print_green(
- f"Success! Multi-account config file written to: {os.path.relpath(filename)}"
+ f"Success! Multi-account config file written to: {os.path.relpath(output_file)}"
)
print(
- f"\nMake sure you edit the {os.path.relpath(filename)} file and then run the scan-multi-account command, as shown below."
+ f"\nMake sure you edit the {os.path.relpath(output_file)} file and then run the scan-multi-account command, as shown below."
)
print(
- f"\n\tcloudsplaining scan-multi-account --exclusions-file exclusions.yml -c {os.path.relpath(filename)} -o ./"
+ f"\n\tcloudsplaining scan-multi-account --exclusions-file exclusions.yml -c {os.path.relpath(output_file)} -o ./"
)
diff --git a/cloudsplaining/command/download.py b/cloudsplaining/command/download.py
index 5f4a87a8..5bdab316 100644
--- a/cloudsplaining/command/download.py
+++ b/cloudsplaining/command/download.py
@@ -7,7 +7,7 @@
# or https://opensource.org/licenses/BSD-3-Clause
import json
import logging
-from pathlib import Path
+import os
from typing import Dict, List, Any
import boto3
@@ -22,29 +22,10 @@
short_help="Runs aws iam get-authorization-details on all accounts specified in the aws credentials "
"file, and stores them in account-alias.json"
)
-@click.option(
- "--profile",
- "-p",
- type=str,
- required=False,
- help="Specify 'all' to authenticate to AWS and analyze *all* existing IAM policies. Specify a non-default "
- "profile here. Defaults to the 'default' profile.",
-)
-@click.option(
- "--output",
- "-o",
- type=click.Path(exists=True),
- default=Path.cwd(),
- help="Path to store the output. Defaults to current directory.",
-)
-@click.option(
- "--include-non-default-policy-versions",
- is_flag=True,
- default=False,
- help="When downloading AWS managed policy documents, also include the non-default policy versions."
- " Note that this will dramatically increase the size of the downloaded file.",
-)
-@click.option("--verbose", "-v", "verbosity", count=True)
+@click.option("-p", "--profile", type=str, required=False, envvar="AWS_DEFAULT_PROFILE", help="Specify 'all' to authenticate to AWS and scan from *all* AWS credentials profiles. Specify a non-default profile here. Defaults to the 'default' profile.")
+@click.option("-o", "--output", type=click.Path(exists=True), default=os.getcwd(), help="Path to store the output. Defaults to current directory.")
+@click.option("--include-non-default-policy-versions", is_flag=True, default=False, help="When downloading AWS managed policy documents, also include the non-default policy versions. Note that this will dramatically increase the size of the downloaded file.")
+@click.option("-v", "--verbose", "verbosity", help="Log verbosity level.", count=True)
def download(
profile: str, output: str, include_non_default_policy_versions: bool, verbosity: int
) -> int:
@@ -59,15 +40,16 @@ def download(
if profile:
session_data["profile_name"] = profile
- output_filename = Path(output) / f"{profile}.json"
+ output_filename = os.path.join(output, f"{profile}.json")
else:
- output_filename = Path("default.json")
+ output_filename = os.path.join(output, f"default.json")
results = get_account_authorization_details(
session_data, include_non_default_policy_versions
)
-
- output_filename.write_text(json.dumps(results, indent=4, default=str))
+ with open(output_filename, "w") as f:
+ json.dump(results, f, indent=4, default=str)
+ # output_filename.write_text(json.dumps(results, indent=4, default=str))
print(f"Saved results to {output_filename}")
return 1
diff --git a/cloudsplaining/command/expand_policy.py b/cloudsplaining/command/expand_policy.py
index ea54d7e7..7fd32595 100644
--- a/cloudsplaining/command/expand_policy.py
+++ b/cloudsplaining/command/expand_policy.py
@@ -18,14 +18,8 @@
@click.command(
short_help="Expand the * Actions in IAM policy files to improve readability"
)
-@click.option(
- "--input-file",
- "-i",
- type=click.Path(exists=True),
- required=True,
- help="Path to the JSON policy file.",
-)
-@click.option("--verbose", "-v", "verbosity", count=True)
+@click.option("-i", "--input-file", type=click.Path(exists=True), required=True, help="Path to the JSON policy file.")
+@click.option("-v", "--verbose", "verbosity", help="Log verbosity level.", count=True)
def expand_policy(input_file: str, verbosity: int) -> None:
"""
Expand the * Actions in IAM policy files to improve readability
diff --git a/cloudsplaining/command/scan.py b/cloudsplaining/command/scan.py
index b40ca5f6..23fd275b 100644
--- a/cloudsplaining/command/scan.py
+++ b/cloudsplaining/command/scan.py
@@ -24,54 +24,17 @@
from cloudsplaining.output.report import HTMLReport
from cloudsplaining import set_log_level
-logger = logging.getLogger(__name__)
-
@click.command(
short_help="Scan a single file containing AWS IAM account authorization details and generate report on "
"IAM security posture. "
)
-@click.option(
- "--input-file",
- "-i",
- type=click.Path(exists=True),
- required=True,
- help="Path of IAM account authorization details file",
-)
-@click.option(
- "--exclusions-file",
- "-e",
- help="A yaml file containing a list of policy names to exclude from the scan.",
- type=click.Path(exists=True),
- required=False,
- default=EXCLUSIONS_FILE,
-)
-@click.option(
- "--output",
- "-o",
- required=False,
- type=click.Path(exists=True),
- default=os.getcwd(),
- help="Output directory.",
-)
-@click.option(
- "--skip-open-report",
- "-s",
- required=False,
- default=False,
- is_flag=True,
- help="Don't open the HTML report in the web browser after creating. "
- "This helps when running the report in automation.",
-)
-@click.option(
- "--minimize",
- "-m",
- required=False,
- default=False,
- is_flag=True,
- help="Reduce the size of the HTML Report by pulling the Cloudsplaining Javascript code over the internet.",
-)
-@click.option("--verbose", "-v", "verbosity", count=True)
+@click.option("-i", "--input-file", type=click.Path(exists=True), required=True, help="Path of IAM account authorization details file")
+@click.option("-e", "--exclusions-file", help="A yaml file containing a list of policy names to exclude from the scan.", type=click.Path(exists=True), required=False, default=EXCLUSIONS_FILE)
+@click.option("-o", "--output", required=False, type=click.Path(exists=True), default=os.getcwd(), help="Output directory.")
+@click.option("-s", "--skip-open-report", required=False, default=False, is_flag=True, help="Don't open the HTML report in the web browser after creating. This helps when running the report in automation.")
+@click.option("-m", "--minimize", required=False, default=False, is_flag=True, help="Reduce the size of the HTML Report by pulling the Cloudsplaining Javascript code over the internet.")
+@click.option("-v", "--verbose", "verbosity", help="Log verbosity level.", count=True)
def scan(
input_file: str,
exclusions_file: str,
@@ -98,7 +61,7 @@ def scan(
exclusions = DEFAULT_EXCLUSIONS
if os.path.isfile(input_file):
- account_name = Path(input_file).stem
+ account_name = os.path.basename(input_file).split(".")[0]
with open(input_file) as f:
contents = f.read()
account_authorization_details_cfg = json.loads(contents)
@@ -137,7 +100,7 @@ def scan(
contents = f.read()
account_authorization_details_cfg = json.loads(contents)
- account_name = Path(file).stem
+ account_name = os.path.basename(input_file).split(".")[0]
# Scan the Account Authorization Details config
rendered_html_report = scan_account_authorization_details(
account_authorization_details_cfg,
@@ -164,6 +127,9 @@ def scan(
webbrowser.open(url, new=2)
+logger = logging.getLogger(__name__)
+
+
def scan_account_authorization_details(
account_authorization_details_cfg: Dict[str, Any],
exclusions: Exclusions,
diff --git a/cloudsplaining/command/scan_multi_account.py b/cloudsplaining/command/scan_multi_account.py
index 58ad6c0c..d5bdf72f 100644
--- a/cloudsplaining/command/scan_multi_account.py
+++ b/cloudsplaining/command/scan_multi_account.py
@@ -41,71 +41,16 @@ def _accounts(self) -> Dict[str, str]:
@click.command(short_help="Scan multiple AWS Accounts using a config file")
-@click.option(
- "--config",
- "-c",
- "config_file",
- type=click.Path(exists=True),
- required=True,
- help="Path of the multi-account config file",
-)
-@click.option(
- "--profile",
- "-p",
- "profile",
- type=str,
- required=False,
- help="Specify the AWS IAM profile.",
- envvar="AWS_PROFILE",
-)
-@click.option(
- "--role-name",
- "-r",
- "role_name",
- type=str,
- required=True,
- help="The name of the IAM role to assume in target accounts. Must be the same name in all target accounts.",
-)
-@click.option(
- "--exclusions-file",
- "-e",
- "exclusions_file",
- help="A yaml file containing a list of policy names to exclude from the scan.",
- type=click.Path(exists=True),
- required=False,
- default=EXCLUSIONS_FILE,
-)
-@optgroup.group(
- "Output Target Options", help="",
-)
-@optgroup.option(
- "--output-directory",
- "-o",
- "output_directory",
- type=click.Path(exists=True),
- help="Output directory. Supply this and/or --bucket.",
-)
-@optgroup.option(
- "--output-bucket",
- "-b",
- "output_bucket",
- type=str,
- help="The S3 bucket to save the results. Supply this and/or --output-directory.",
-)
-@optgroup.group(
- "Other Options", help="",
-)
-@optgroup.option(
- "--write-data-file",
- "-w",
- is_flag=True,
- required=False,
- default=False,
- help="Save the cloudsplaining JSON-formatted data results.",
-)
-@click.option(
- "-v", "--verbose", "verbosity", count=True,
-)
+@click.option("--config", "-c", "config_file", type=click.Path(exists=True), required=True, help="Path of the multi-account config file")
+@click.option("-p", "--profile", type=str, required=False, envvar="AWS_DEFAULT_PROFILE", help="Specify the AWS IAM profile")
+@click.option("-r", "--role-name", "role_name", type=str, required=True, help="The name of the IAM role to assume in target accounts. Must be the same name in all target accounts.")
+@click.option("-e", "--exclusions-file", "exclusions_file", help="A yaml file containing a list of policy names to exclude from the scan.", type=click.Path(exists=True), required=False, default=EXCLUSIONS_FILE)
+@optgroup.group("Output Target Options", help="")
+@optgroup.option("-o", "--output-directory", "output_directory", type=click.Path(exists=True), help="Output directory. Supply this and/or --bucket.")
+@optgroup.option("-b", "--output-bucket", "output_bucket", type=str, help="The S3 bucket to save the results. Supply this and/or --output-directory.")
+@optgroup.group("Other Options", help="")
+@optgroup.option("-w", "--write-data-file", is_flag=True, required=False, default=False, help="Save the cloudsplaining JSON-formatted data results.")
+@click.option("-v", "--verbose", "verbosity", help="Log verbosity level.", count=True)
def scan_multi_account(
config_file: str,
profile: str,
@@ -191,8 +136,8 @@ def scan_accounts(
)
if output_directory:
# Write the HTML file
- html_output_file = Path(output_directory) / f"{target_account_name}.html"
- html_output_file.write_text(rendered_report)
+ html_output_file = os.path.join(output_directory, f"{target_account_name}.html")
+ utils.write_file(html_output_file, rendered_report)
utils.print_green(
f"Saved the HTML report to: {os.path.relpath(html_output_file)}"
)
diff --git a/cloudsplaining/command/scan_policy_file.py b/cloudsplaining/command/scan_policy_file.py
index 67cb7d3e..9b9d2990 100644
--- a/cloudsplaining/command/scan_policy_file.py
+++ b/cloudsplaining/command/scan_policy_file.py
@@ -28,29 +28,9 @@
@click.command(
short_help="Scan a single policy file to identify identify missing resource constraints."
)
-@click.option(
- "--input-file",
- "-i",
- type=str,
- # required=True,
- help="Path of the IAM policy file to evaluate.",
-)
-@click.option(
- "--exclusions-file",
- "-e",
- help="A yaml file containing a list of actions to ignore when scanning.",
- type=click.Path(exists=True),
- required=False,
- default=EXCLUSIONS_FILE,
-)
-@click.option(
- "--high-priority-only",
- required=False,
- default=False,
- is_flag=True,
- help="If issues are found, only print the high priority risks"
- " (Resource Exposure, Privilege Escalation, Data Exfiltration). This can help with prioritization.",
-)
+@click.option("-i", "--input-file", type=str, help="Path of the IAM policy file to evaluate.")
+@click.option("-e", "--exclusions-file", help="A yaml file containing a list of actions to ignore when scanning.", type=click.Path(exists=True), required=False, default=EXCLUSIONS_FILE)
+@click.option("--high-priority-only", required=False, default=False, is_flag=True, help="If issues are found, only print the high priority risks (Resource Exposure, Privilege Escalation, Data Exfiltration). This can help with prioritization.")
@click.option("--verbose", "-v", "verbosity", count=True)
# pylint: disable=redefined-builtin
def scan_policy_file(
diff --git a/cloudsplaining/output/report.py b/cloudsplaining/output/report.py
index 808f3e2f..c4fcb6c5 100644
--- a/cloudsplaining/output/report.py
+++ b/cloudsplaining/output/report.py
@@ -1,13 +1,14 @@
"""Creates the HTML Reports"""
import json
import datetime
+import os.path
from pathlib import Path
from typing import Dict, Any
from jinja2 import Environment, FileSystemLoader
from cloudsplaining.bin.version import __version__
-app_bundle_path = Path(__file__).parent / "dist" / "js" / "index.js"
+app_bundle_path = os.path.join(os.path.dirname(__file__), "dist", "js", "index.js")
class HTMLReport:
@@ -35,7 +36,9 @@ def app_bundle(self) -> str:
bundle = f''
return bundle
else:
- bundle_content = app_bundle_path.read_text(encoding="utf-8")
+ with open(app_bundle_path, "r", encoding="utf-8") as f:
+ bundle_content = f.read()
+ # bundle_content = app_bundle_path.read_text(encoding="utf-8")
bundle = f''
return bundle
@@ -50,7 +53,9 @@ def vendor_bundle(self) -> str:
return bundle
else:
vendor_bundle_path = get_vendor_bundle_path()
- bundle_content = vendor_bundle_path.read_text(encoding="utf-8")
+ with open(vendor_bundle_path, "r", encoding="utf-8") as f:
+ bundle_content = f.read()
+ # bundle_content = vendor_bundle_path.read_text(encoding="utf-8")
bundle = f''
return bundle
@@ -67,18 +72,20 @@ def get_html_report(self) -> str:
report_generated_time=str(self.report_generated_time),
cloudsplaining_version=__version__,
)
- template_path = Path(__file__).parent
+ template_path = os.path.dirname(__file__)
env = Environment(loader=FileSystemLoader(template_path)) # nosec
template = env.get_template("template.html")
return template.render(t=template_contents)
-def get_vendor_bundle_path() -> Path:
+def get_vendor_bundle_path() -> str:
"""Finds the vendored javascript bundle even if it has a hash suffix"""
- vendor_bundle_directory = Path(__file__).parent / "dist" / "js"
- file_list_with_full_path = [
- file.absolute()
- for file in vendor_bundle_directory.glob("*.js")
- if file.name.startswith("chunk-vendors.")
- ]
+ vendor_bundle_directory = os.path.join(os.path.dirname(__file__), "dist", "js")
+ file_list_with_full_path = []
+ for f in os.listdir(vendor_bundle_directory):
+ file_path = os.path.join(vendor_bundle_directory, f)
+ if os.path.isfile(file_path):
+ if os.path.splitext(file_path)[-1].endswith("js"):
+ if os.path.splitext(f)[0].startswith("chunk-vendors"):
+ file_list_with_full_path.append(os.path.abspath(file_path))
return file_list_with_full_path[0]
diff --git a/cloudsplaining/shared/utils.py b/cloudsplaining/shared/utils.py
index b5f88d6c..35cd24a5 100644
--- a/cloudsplaining/shared/utils.py
+++ b/cloudsplaining/shared/utils.py
@@ -4,6 +4,7 @@
# Licensed under the BSD 3-Clause license.
# For full license text, see the LICENSE file in the repo root
# or https://opensource.org/licenses/BSD-3-Clause
+import os
import json
import logging
from hashlib import sha256
@@ -130,7 +131,8 @@ def write_results_data_file(
:return:
"""
# Write the output to a results file if that was specified. Otherwise, just print to stdout
- Path(raw_data_file).write_text(json.dumps(results, indent=4, default=str))
+ with open(raw_data_file, "w") as f:
+ f.write(json.dumps(results, indent=4, default=str))
return raw_data_file
@@ -148,3 +150,21 @@ def print_green(string: Any) -> None:
def print_grey(string: Any) -> None:
"""Print grey text"""
print(f"{GREY}{string}{END}")
+
+
+def write_file(file: str, content: str) -> None:
+ if os.path.exists(file):
+ logger.debug("%s exists. Removing the file and replacing its contents." % file)
+ os.remove(file)
+ with open(file, "w") as f:
+ f.write(content)
+
+
+def write_json_to_file(file: str, content: str) -> None:
+ if os.path.exists(file):
+ logger.debug("%s exists. Removing the file and replacing its contents." % file)
+ os.remove(file)
+
+ with open(file, "w") as f:
+ json.dump(content, f, indent=4, default=str)
+