Skip to content

Commit c8af1df

Browse files
committed
drop Python 3.9 support
1 parent 1c64b16 commit c8af1df

31 files changed

+314
-371
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ permissions:
1111
contents: read
1212

1313
env:
14-
MIN_PYTHON_VERSION: "3.9"
14+
MIN_PYTHON_VERSION: "3.10"
1515

1616
jobs:
1717
test:

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ permissions:
1212
contents: read
1313

1414
env:
15-
MIN_PYTHON_VERSION: "3.9"
15+
MIN_PYTHON_VERSION: "3.10"
1616

1717
jobs:
1818
pre-commit:
@@ -60,7 +60,7 @@ jobs:
6060
strategy:
6161
fail-fast: true
6262
matrix:
63-
python: ['3.10', '3.11', '3.12', '3.13', '3.14']
63+
python: ['3.11', '3.12', '3.13', '3.14']
6464
steps:
6565
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
6666
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0

.github/workflows/update-bundle-report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ permissions:
1010
contents: read
1111

1212
env:
13-
MIN_PYTHON_VERSION: "3.9"
13+
MIN_PYTHON_VERSION: "3.10"
1414

1515
jobs:
1616
update:

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.9
1+
3.10

.readthedocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ version: 2
77
build:
88
os: ubuntu-24.04
99
tools:
10-
python: "3.9"
10+
python: "3.10"
1111

1212
mkdocs:
1313
configuration: mkdocs.yml

cloudsplaining/command/create_exclusions_file.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# For full license text, see the LICENSE file in the repo root
1010
# or https://opensource.org/licenses/BSD-3-Clause
1111
import logging
12-
import os
12+
from pathlib import Path
1313

1414
import click
1515

@@ -21,14 +21,14 @@
2121

2222

2323
@click.command(
24-
context_settings=dict(max_content_width=160),
24+
context_settings={"max_content_width": 160},
2525
short_help="Creates a YML file to be used as a custom exclusions template",
2626
)
2727
@click.option(
2828
"-o",
2929
"--output-file",
3030
type=click.Path(exists=False),
31-
default=os.path.join(os.getcwd(), "exclusions.yml"),
31+
default=str(Path.cwd() / "exclusions.yml"),
3232
required=True,
3333
help="Relative path to output file where we want to store the exclusions template.",
3434
)
@@ -40,7 +40,7 @@ def create_exclusions_file(output_file: str, verbosity: int) -> None:
4040
"""
4141
set_log_level(verbosity)
4242

43-
with open(output_file, "a", encoding="utf-8") as file_obj:
43+
with Path(output_file).open("a", encoding="utf-8") as file_obj:
4444
file_obj.write(EXCLUSIONS_TEMPLATE)
4545

4646
utils.print_green(f"Success! Exclusions template file written to: {output_file}")

cloudsplaining/command/create_multi_account_config_file.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# For full license text, see the LICENSE file in the repo root
1010
# or https://opensource.org/licenses/BSD-3-Clause
1111
import logging
12-
import os
12+
from pathlib import Path
1313

1414
import click
1515

@@ -23,15 +23,15 @@
2323

2424

2525
@click.command(
26-
context_settings=dict(max_content_width=160),
26+
context_settings={"max_content_width": 160},
2727
short_help="Creates a YML file to be used for multi-account scanning",
2828
)
2929
@click.option(
3030
"-o",
3131
"--output-file",
3232
"output_file",
3333
type=click.Path(exists=False),
34-
default=os.path.join(os.getcwd(), "multi-account-config.yml"),
34+
default=str(Path.cwd() / "multi-account-config.yml"),
3535
required=True,
3636
help="Relative path to output file where we want to store the multi account config template.",
3737
)
@@ -42,17 +42,14 @@ def create_multi_account_config_file(output_file: str, verbosity: int) -> None:
4242
"""
4343
set_log_level(verbosity)
4444

45-
if os.path.exists(output_file):
45+
output_file = Path(output_file)
46+
if output_file.exists():
4647
logger.debug("%s exists. Removing the file and replacing its contents.", output_file)
47-
os.remove(output_file)
48+
output_file.unlink()
4849

49-
with open(output_file, "a", encoding="utf-8") as file_obj:
50+
with output_file.open("a", encoding="utf-8") as file_obj:
5051
file_obj.write(MULTI_ACCOUNT_CONFIG_TEMPLATE)
5152

52-
utils.print_green(f"Success! Multi-account config file written to: {os.path.relpath(output_file)}")
53-
print(
54-
f"\nMake sure you edit the {os.path.relpath(output_file)} file and then run the scan-multi-account command, as shown below."
55-
)
56-
print(
57-
f"\n\tcloudsplaining scan-multi-account --exclusions-file exclusions.yml -c {os.path.relpath(output_file)} -o ./"
58-
)
53+
utils.print_green(f"Success! Multi-account config file written to: {output_file}")
54+
print(f"\nMake sure you edit the {output_file} file and then run the scan-multi-account command, as shown below.")
55+
print(f"\n\tcloudsplaining scan-multi-account --exclusions-file exclusions.yml -c {output_file} -o ./")

cloudsplaining/command/download.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import json
1212
import logging
1313
import os
14+
from pathlib import Path
1415
from typing import TYPE_CHECKING, Any
1516

1617
import boto3
@@ -41,7 +42,7 @@
4142
"-o",
4243
"--output",
4344
type=click.Path(exists=True),
44-
default=os.getcwd(),
45+
default=os.getcwd(), # noqa: PTH109
4546
help="Path to store the output. Defaults to current directory.",
4647
)
4748
@click.option(
@@ -61,14 +62,15 @@ def download(profile: str, output: str, include_non_default_policy_versions: boo
6162
default_region = "us-east-1"
6263
session_data = {"region_name": default_region}
6364

65+
output = Path(output)
6466
if profile:
6567
session_data["profile_name"] = profile
66-
output_filename = os.path.join(output, f"{profile}.json")
68+
output_filename = output / f"{profile}.json"
6769
else:
68-
output_filename = os.path.join(output, "default.json")
70+
output_filename = output / "default.json"
6971

7072
results = get_account_authorization_details(session_data, include_non_default_policy_versions)
71-
with open(output_filename, "w", encoding="utf-8") as f:
73+
with output_filename.open("w", encoding="utf-8") as f:
7274
json.dump(results, f, indent=4, default=str)
7375
# output_filename.write_text(json.dumps(results, indent=4, default=str))
7476
print(f"Saved results to {output_filename}")

cloudsplaining/command/expand_policy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# or https://opensource.org/licenses/BSD-3-Clause
1010
import json
1111
import logging
12+
from pathlib import Path
1213

1314
import click
1415
from policy_sentry.analysis.expand import get_expanded_policy
@@ -33,7 +34,7 @@ def expand_policy(input_file: str, verbosity: int) -> None:
3334
"""
3435
set_log_level(verbosity)
3536

36-
with open(input_file, encoding="utf-8") as json_file:
37+
with Path(input_file).open(encoding="utf-8") as json_file:
3738
logger.debug(f"Opening {input_file}")
3839
data = json.load(json_file)
3940
policy = get_expanded_policy(data)

cloudsplaining/command/scan.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@
4747
help="A yaml file containing a list of policy names to exclude from the scan.",
4848
type=click.Path(exists=True),
4949
required=False,
50-
default=EXCLUSIONS_FILE,
50+
default=str(EXCLUSIONS_FILE),
5151
)
5252
@click.option(
5353
"-o",
5454
"--output",
5555
required=False,
5656
type=click.Path(exists=True),
57-
default=os.getcwd(),
57+
default=os.getcwd(), # noqa: PTH109
5858
help="Output directory.",
5959
)
6060
@click.option(
@@ -123,7 +123,7 @@ def scan(
123123

124124
if exclusions_file:
125125
# Get the exclusions configuration
126-
with open(exclusions_file, encoding="utf-8") as yaml_file:
126+
with Path(exclusions_file).open(encoding="utf-8") as yaml_file:
127127
try:
128128
exclusions_cfg = yaml.safe_load(yaml_file)
129129
except yaml.YAMLError as exc:
@@ -139,9 +139,11 @@ def scan(
139139
flag_conditional_statements = False
140140
flag_resource_arn_statements = False
141141

142-
if os.path.isfile(input_file):
143-
account_name = os.path.basename(input_file).split(".")[0]
144-
account_authorization_details_cfg = json.loads(Path(input_file).read_text(encoding="utf-8"))
142+
output = Path(output)
143+
input_file = Path(input_file)
144+
if input_file.is_file():
145+
account_name = input_file.stem
146+
account_authorization_details_cfg = json.loads(input_file.read_text(encoding="utf-8"))
145147
rendered_html_report = scan_account_authorization_details(
146148
account_authorization_details_cfg,
147149
exclusions,
@@ -154,29 +156,29 @@ def scan(
154156
flag_trust_policies=flag_trust_policies,
155157
severity=severity,
156158
)
157-
html_output_file = os.path.join(output, f"iam-report-{account_name}.html")
159+
html_output_file = output / f"iam-report-{account_name}.html"
158160
logger.info("Saving the report to %s", html_output_file)
159-
if os.path.exists(html_output_file):
160-
os.remove(html_output_file)
161+
if html_output_file.exists():
162+
html_output_file.unlink()
161163

162-
Path(html_output_file).write_text(rendered_html_report, encoding="utf-8")
164+
html_output_file.write_text(rendered_html_report, encoding="utf-8")
163165

164166
print(f"Wrote HTML results to: {html_output_file}")
165167

166168
# Open the report by default
167169
if not skip_open_report:
168170
print("Opening the HTML report")
169-
url = f"file://{os.path.abspath(html_output_file)}"
171+
url = f"file://{html_output_file.absolute()}"
170172
webbrowser.open(url, new=2)
171173

172-
if os.path.isdir(input_file):
174+
if input_file.is_dir():
173175
logger.info("The path given is a directory. Scanning for account authorization files and generating report.")
174176
input_files = get_authorization_files_in_directory(input_file)
175177
for file in input_files:
176178
logger.info(f"Scanning file: {file}")
177179
account_authorization_details_cfg = json.loads(Path(file).read_text(encoding="utf-8"))
178180

179-
account_name = os.path.basename(input_file).split(".")[0]
181+
account_name = input_file.parent.stem
180182
# Scan the Account Authorization Details config
181183
rendered_html_report = scan_account_authorization_details(
182184
account_authorization_details_cfg,
@@ -187,19 +189,19 @@ def scan(
187189
minimize=minimize,
188190
severity=severity,
189191
)
190-
html_output_file = os.path.join(output, f"iam-report-{account_name}.html")
192+
html_output_file = output / f"iam-report-{account_name}.html"
191193
logger.info("Saving the report to %s", html_output_file)
192-
if os.path.exists(html_output_file):
193-
os.remove(html_output_file)
194+
if html_output_file.exists():
195+
html_output_file.unlink()
194196

195-
Path(html_output_file).write_text(rendered_html_report, encoding="utf-8")
197+
html_output_file.write_text(rendered_html_report, encoding="utf-8")
196198

197199
print(f"Wrote HTML results to: {html_output_file}")
198200

199201
# Open the report by default
200202
if not skip_open_report:
201203
print("Opening the HTML report")
202-
url = f"file://{os.path.abspath(html_output_file)}"
204+
url = f"file://{html_output_file.absolute()}"
203205
webbrowser.open(url, new=2)
204206

205207

@@ -211,7 +213,7 @@ def scan_account_authorization_details(
211213
account_authorization_details_cfg: dict[str, Any],
212214
exclusions: Exclusions,
213215
account_name: str,
214-
output_directory: str,
216+
output_directory: str | Path | None,
215217
write_data_files: bool,
216218
minimize: bool,
217219
return_json_results: Literal[True],
@@ -227,7 +229,7 @@ def scan_account_authorization_details(
227229
account_authorization_details_cfg: dict[str, Any],
228230
exclusions: Exclusions,
229231
account_name: str = ...,
230-
output_directory: str = ...,
232+
output_directory: str | Path | None = ...,
231233
write_data_files: bool = ...,
232234
minimize: bool = ...,
233235
return_json_results: Literal[False] = ...,
@@ -242,7 +244,7 @@ def scan_account_authorization_details(
242244
account_authorization_details_cfg: dict[str, Any],
243245
exclusions: Exclusions,
244246
account_name: str = "default",
245-
output_directory: str = os.getcwd(),
247+
output_directory: str | Path | None = None,
246248
write_data_files: bool = False,
247249
minimize: bool = False,
248250
return_json_results: bool = False,
@@ -285,14 +287,13 @@ def scan_account_authorization_details(
285287

286288
# Raw data file
287289
if write_data_files:
288-
if output_directory is None:
289-
output_directory = os.getcwd()
290+
output_directory = Path(output_directory) if output_directory else Path.cwd()
290291

291-
results_data_file = os.path.join(output_directory, f"iam-results-{account_name}.json")
292+
results_data_file = output_directory / f"iam-results-{account_name}.json"
292293
results_data_filepath = write_results_data_file(authorization_details.results, results_data_file)
293294
print(f"Results data saved: {results_data_filepath}")
294295

295-
findings_data_file = os.path.join(output_directory, f"iam-findings-{account_name}.json")
296+
findings_data_file = output_directory / f"iam-findings-{account_name}.json"
296297
findings_data_filepath = write_results_data_file(results, findings_data_file)
297298
print(f"Findings data file saved: {findings_data_filepath}")
298299

@@ -302,15 +303,15 @@ def scan_account_authorization_details(
302303
"iam_findings": results,
303304
"rendered_report": rendered_report,
304305
}
305-
else:
306-
return rendered_report
306+
307+
return rendered_report
307308

308309

309310
def get_authorization_files_in_directory(
310-
directory: str,
311+
directory: Path,
311312
) -> list[str]: # pragma: no cover
312313
"""Get a list of download-account-authorization-files in a directory"""
313-
file_list_with_full_path = [file.absolute() for file in Path(directory).glob("*.json")]
314+
file_list_with_full_path = [file.absolute() for file in directory.glob("*.json")]
314315

315316
new_file_list = []
316317
for file in file_list_with_full_path:

0 commit comments

Comments
 (0)