Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cloudsplaining/command/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@
is_flag=True,
help="Flag risky trust policies in roles.",
)
@click.option(
"-dRT",
"--disable-report-tabs",
required=False,
default=False,
is_flag=True,
help="Disable the Guidance and Appendices tabs in the report to reduce file size.",
)
def scan(
input_file: str,
exclusions_file: str,
Expand All @@ -114,6 +122,7 @@ def scan(
verbosity: int,
severity: list[str],
flag_trust_policies: bool,
disable_report_tabs: bool,
) -> None: # pragma: no cover
"""
Given the path to account authorization details files and the exclusions config file, scan all inline and
Expand Down Expand Up @@ -153,6 +162,7 @@ def scan(
flag_resource_arn_statements=flag_resource_arn_statements,
flag_trust_policies=flag_trust_policies,
severity=severity,
disable_report_tabs=disable_report_tabs,
)
html_output_file = os.path.join(output, f"iam-report-{account_name}.html")
logger.info("Saving the report to %s", html_output_file)
Expand Down Expand Up @@ -186,6 +196,7 @@ def scan(
write_data_files=True,
minimize=minimize,
severity=severity,
disable_report_tabs=disable_report_tabs,
)
html_output_file = os.path.join(output, f"iam-report-{account_name}.html")
logger.info("Saving the report to %s", html_output_file)
Expand Down Expand Up @@ -219,6 +230,7 @@ def scan_account_authorization_details(
flag_resource_arn_statements: bool = ...,
flag_trust_policies: bool = ...,
severity: list[str] | None = ...,
disable_report_tabs: bool = ...,
) -> dict[str, Any]: ...


Expand All @@ -235,6 +247,7 @@ def scan_account_authorization_details(
flag_resource_arn_statements: bool = ...,
flag_trust_policies: bool = ...,
severity: list[str] | None = ...,
disable_report_tabs: bool = ...,
) -> str: ...


Expand All @@ -250,6 +263,7 @@ def scan_account_authorization_details(
flag_resource_arn_statements: bool = False,
flag_trust_policies: bool = False,
severity: list[str] | None = None,
disable_report_tabs: bool = False,
) -> str | dict[str, Any]: # pragma: no cover
"""
Given the path to account authorization details files and the exclusions config file, scan all inline and
Expand Down Expand Up @@ -280,6 +294,7 @@ def scan_account_authorization_details(
account_name=account_name,
results=results,
minimize=minimize,
disable_report_tabs=disable_report_tabs, # Passed to HTMLReport
)
rendered_report = html_report.get_html_report()

Expand Down
8 changes: 4 additions & 4 deletions cloudsplaining/output/dist/js/index.js

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions cloudsplaining/output/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ def __init__(
account_name: str,
results: dict[str, dict[str, Any]],
minimize: bool = False,
disable_report_tabs: bool = False,
) -> None:
self.account_name = account_name
self.account_id = account_id
self.report_generated_time = datetime.datetime.now().strftime("%Y-%m-%d")
self.minimize = minimize
self.results = f"var iam_data = {json.dumps(results, default=str)}"
self.template_config = TemplateConfig()
self.disable_report_tabs = disable_report_tabs

@property
def app_bundle(self) -> str:
Expand Down Expand Up @@ -65,6 +67,17 @@ def vendor_bundle(self) -> str:

def get_html_report(self) -> str:
"""Returns the rendered HTML report"""
if self.disable_report_tabs:
guidance_content = ""
appendices_content = ""
show_guidance_nav = False
show_appendices_nav = False
else:
guidance_content = self.template_config.guidance_content
appendices_content = self.template_config.appendices_content
show_guidance_nav = self.template_config.show_guidance_nav
show_appendices_nav = self.template_config.show_appendices_nav

template_contents = dict(
vendor_bundle_js=self.vendor_bundle,
app_bundle_js=self.app_bundle,
Expand All @@ -75,10 +88,11 @@ def get_html_report(self) -> str:
account_name=self.account_name,
report_generated_time=str(self.report_generated_time),
cloudsplaining_version=__version__,
guidance_content=self.template_config.guidance_content,
appendices_content=self.template_config.appendices_content,
show_guidance_nav=self.template_config.show_guidance_nav,
show_appendices_nav=self.template_config.show_appendices_nav,
# USE THE NEW CONDITIONAL VARIABLES:
guidance_content=guidance_content,
appendices_content=appendices_content,
show_guidance_nav=show_guidance_nav,
show_appendices_nav=show_appendices_nav,
)
template_path = os.path.dirname(__file__)
env = Environment(loader=FileSystemLoader(template_path)) # noqa: S701
Expand Down
117 changes: 112 additions & 5 deletions cloudsplaining/output/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@
<!-- <b-nav-item to="/task-table">Task Table Demo</b-nav-item> -->
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-text
><strong>Account ID:</strong> {{ account_id }} |
<strong>Account Name:</strong>
{{ account_name }}</b-nav-text
>
<!-- START NEW ADDITION: CSV Export Button -->
<b-nav-item @click="exportToCSV" class="mr-2">
<i class="fas fa-file-csv"></i> Export CSV
</b-nav-item>
<!-- END NEW ADDITION -->
<b-nav-text
><strong>Account ID:</strong> {{ account_id }} |
<strong>Account Name:</strong>
{{ account_name }}</b-nav-text
>
</b-navbar-nav>
</b-collapse>
</b-navbar>
Expand Down Expand Up @@ -171,6 +176,108 @@ export default {
scrollFix: function (hashbang) {
location.hash = hashbang;
},
csvEscape(field) {
if (field == null) {
return '""';
}
// Convert to string and replace double quotes with two double quotes
let str = String(field);
str = str.replace(/"/g, '""');
// Always wrap in double quotes to handle commas, semicolons, and escaped double quotes
return `"${str}"`;
},
exportToCSV() {
// Headers requested in issue #186 for Privilege Escalation findings
const headers = [
"Account",
"Principal Name",
"Principal Type",
"Policy Name(s)",
"Policy Type",
"Privesc Methods Identified"
];

// This array will hold the flattened data rows, starting with headers
let csvRows = [headers.map(h => this.csvEscape(h)).join(',')];
const accountId = this.account_id;
const data = this.iam_data;

// Principal types to iterate through
const principalTypes = ["Roles", "Users", "Groups"];

principalTypes.forEach(principalType => {
// Determine singular type name for the CSV column (e.g., "Role" from "Roles")
const singularPrincipalType = principalType.slice(0, -1);
const principals = data[principalType] || {};

for (const principalName in principals) {
const principal = principals[principalName];

// Check if this principal has any privilege escalation findings
if (principal.findings.privilege_escalation &&
principal.findings.privilege_escalation.methods_identified &&
principal.findings.privilege_escalation.methods_identified.length > 0) {

// Join methods with a semicolon to avoid conflicts with CSV commas
const privescMethods = principal.findings.privilege_escalation.methods_identified.join('; ');
const policies = principal.policies || {};

let policyNames = [];
let policyTypes = [];

// Collect policy names and types
for (const policyName in policies) {
const policy = policies[policyName];
policyNames.push(policyName);
// Determine policy type (Inline vs Managed)
if (policy.is_inline) {
policyTypes.push("Inline");
} else {
policyTypes.push("Managed");
}
}

// Deduplicate and join policy types
const uniquePolicyTypes = [...new Set(policyTypes)].join('; ');

// Build the row array
const row = [
accountId,
principalName,
singularPrincipalType,
policyNames.join('; '),
uniquePolicyTypes,
privescMethods
];

// Join and escape all fields for the CSV row
csvRows.push(row.map(field => this.csvEscape(field)).join(','));
}
}
});

// Trigger the download of the CSV file
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");

if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
// Set the filename
link.setAttribute("download", `cloudsplaining-privesc-export-${Date.now()}.csv`);

// Programmatically click the link to start the download
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
console.error("Browser does not support direct CSV download.");
alert("Your browser does not support downloading CSV files directly.");
}
},
// END NEW ADDITION
},
provide() {
return {
Expand Down
Loading